From b8cc059df8f7ca1f6484be763986704194894c4c Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 21 Jun 2023 00:30:24 +0200 Subject: [PATCH 01/11] Add user relation to geoprovider --- .../migration.sql | 6 ++++++ apps/server/prisma/schema.prisma | 15 +++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql diff --git a/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql b/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql new file mode 100644 index 000000000..d6ba5fc8c --- /dev/null +++ b/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql @@ -0,0 +1,6 @@ +-- Step 1: Add the "userId" column to the "GeoEventProvider" table +ALTER TABLE "GeoEventProvider" +ADD COLUMN "userId" TEXT; + +-- AddForeignKey +ALTER TABLE "GeoEventProvider" ADD CONSTRAINT "GeoEventProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index bc414a11b..7c82a2fc8 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -10,23 +10,24 @@ datasource db { } model User { - id String @id @default(cuid()) - sub String? @unique + id String @id @default(cuid()) + sub String? @unique name String? - email String @unique - emailVerified Boolean @default(false) + email String @unique + emailVerified Boolean @default(false) detectionMethods Json // ["MODIS","VIIRS","LANDSAT","GEOSTATIONARY"] isPlanetRO Boolean? image String? deletedAt DateTime? isVerified Boolean? lastLogin DateTime? - signupDate DateTime @default(now()) - roles Role @default(ROLE_CLIENT) + signupDate DateTime @default(now()) + roles Role @default(ROLE_CLIENT) alertMethods AlertMethod[] projects Project[] sites Site[] remoteId String? + GeoEventProvider GeoEventProvider[] } model VerificationRequest { @@ -102,6 +103,8 @@ model GeoEventProvider { isActive Boolean lastRun DateTime? config Json + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) } model GeoEvent { From 177cb749863ac3734dad662376cb1cb4fb52a42c Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 21 Jun 2023 02:13:22 +0200 Subject: [PATCH 02/11] Add API Routes for GeoEvent Providers With these routes Users can create Client ID and API key to send data to FireAlert --- .../server/api/routers/geoEventProvider.ts | 165 ++++++++++++------ apps/server/src/server/api/routers/user.ts | 7 +- .../api/zodSchemas/geoEventProvider.schema.ts | 22 +-- 3 files changed, 118 insertions(+), 76 deletions(-) diff --git a/apps/server/src/server/api/routers/geoEventProvider.ts b/apps/server/src/server/api/routers/geoEventProvider.ts index 6685f1afc..188bbff98 100644 --- a/apps/server/src/server/api/routers/geoEventProvider.ts +++ b/apps/server/src/server/api/routers/geoEventProvider.ts @@ -4,31 +4,53 @@ import { createTRPCRouter, protectedProcedure, } from "../trpc"; -import {ensureAdmin} from '../../../utils/routers/trpc' +import { randomUUID } from "crypto"; +import { type TRPCContext } from "../../../../src/Interfaces/Context"; -// Every procedure in geoEventProvider Router must be an admin only procedure -// We implement this check by using the ensureAdmin function +// Users + +export function checkUserOwnsProvider (ctx: TRPCContext, id: string) { + return ctx.prisma.geoEventProvider.findFirst({ + where: { + id: id, + userId: ctx.user.id, + } + }); +} export const geoEventProviderRouter = createTRPCRouter({ - createGeoEventProvider: protectedProcedure + create: protectedProcedure .input(createGeoEventProviderSchema) .mutation(async ({ ctx, input }) => { - ensureAdmin(ctx) try { - const { type, isActive, providerKey, config } = input; + const { name, description, isActive } = input; + const userId = ctx.user.id ?? null; const geoEventProvider = await ctx.prisma.geoEventProvider.create({ data: { - type, + name, + description, + type: "fire", isActive, - providerKey, - config, + clientApiKey: randomUUID(), + clientId: randomUUID(), + fetchFrequency: null, + config: {}, + userId: userId, + }, + select: { + id: true, + name: true, + description: true, + type: true, + isActive: true, + clientApiKey: true, + clientId: true, }, }); - return { - status: "success", - data: geoEventProvider, - }; + + return geoEventProvider; + } catch (error) { console.log(error); throw new TRPCError({ @@ -38,85 +60,112 @@ export const geoEventProviderRouter = createTRPCRouter({ } }), - updateGeoEventProvider: protectedProcedure + update: protectedProcedure .input(updateGeoEventProviderSchema) .mutation(async ({ ctx, input }) => { - ensureAdmin(ctx) try { - const { params, body } = input; - - const geoEventProvider = await ctx.prisma.geoEventProvider.findUnique({ - where: { - id: params.id, - }, - }); - + const id = input.params.id; + const body = input.body; + const userId = ctx.user.id; + //check if user owns the geoEventProvider + const geoEventProvider = await checkUserOwnsProvider(ctx, id); if (!geoEventProvider) { throw new TRPCError({ code: "NOT_FOUND", - message: "GeoEventProvider with that id does not exist, cannot update GeoEventProvider", + message: "GeoEventProvider with that id does not exist", }); } const updatedGeoEventProvider = await ctx.prisma.geoEventProvider.update({ where: { - id: params.id, + id: id, }, data: body, + select: { + id: true, + name: true, + description: true, + type: true, + isActive: true, + clientApiKey: true, + clientId: true, + } }); - return { - status: "success", - data: updatedGeoEventProvider, - }; + return updatedGeoEventProvider; } catch (error) { console.log(error); throw new TRPCError({ - code: "CONFLICT", + code: `${error.code}}`, message: `${error}`, }); } }), - getGeoEventProviders: protectedProcedure + list: protectedProcedure .query(async ({ ctx }) => { - ensureAdmin(ctx) try { - const geoEventProviders = await ctx.prisma.geoEventProvider.findMany(); + const geoEventProviders = await ctx.prisma.geoEventProvider.findMany( + { + where: { + userId: ctx.user.id, + }, + select: { + id: true, + name: true, + description: true, + type: true, + isActive: true, + clientApiKey: true, + clientId: true, + } + } + ); - return { - status: "success", - data: geoEventProviders, - }; + return geoEventProviders; } catch (error) { console.log(error); throw new TRPCError({ - code: "CONFLICT", + code: `${error.code}}`, message: `${error}`, }); } }), - getGeoEventProvider: protectedProcedure + rollApiKey: protectedProcedure .input(geoEventProviderParamsSchema) - .query(async ({ ctx, input }) => { - ensureAdmin(ctx) + .mutation(async ({ ctx, input }) => { try { - const geoEventProvider = await ctx.prisma.geoEventProvider.findUnique({ - where: { - id: input.id, - }, - }); - + //check if user owns the geoEventProvider + const geoEventProvider = await checkUserOwnsProvider(ctx, input.id); if (!geoEventProvider) { throw new TRPCError({ code: "NOT_FOUND", message: "GeoEventProvider with that id does not exist", }); } + //roll Api Key + const updatedGeoEventProvider = await ctx.prisma.geoEventProvider.update({ + where: { + id: input.id + }, + data: { + clientApiKey: randomUUID(), + }, + select: { + id: true, + name: true, + description: true, + type: true, + isActive: true, + clientApiKey: true, + clientId: true, + }, + }); + return { status: "success", - data: geoEventProvider, + data: updatedGeoEventProvider, }; } catch (error) { console.log(error); @@ -127,22 +176,24 @@ export const geoEventProviderRouter = createTRPCRouter({ } }), - deleteGeoEventProvider: protectedProcedure + delete: protectedProcedure .input(geoEventProviderParamsSchema) .mutation(async ({ ctx, input }) => { - ensureAdmin(ctx) try { - const deletedGeoEventProvider = await ctx.prisma.geoEventProvider.delete({ - where: { - id: input.id, - }, - }); - if (!deletedGeoEventProvider) { + //check if user owns the geoEventProvider + const geoEventProvider = await checkUserOwnsProvider(ctx, input.id); + if (!geoEventProvider) { throw new TRPCError({ code: "NOT_FOUND", message: "GeoEventProvider with that id does not exist", }); } + //delete geoEventProvider + const deletedGeoEventProvider = await ctx.prisma.geoEventProvider.delete({ + where: { + id: input.id, + }, + }); return { status: "success", message: `GeoEventProvider with id ${deletedGeoEventProvider.id} has been deleted.`, @@ -150,7 +201,7 @@ export const geoEventProviderRouter = createTRPCRouter({ } catch (error) { console.log(error); throw new TRPCError({ - code: "CONFLICT", + code: `${error.code}`, message: `${error}`, }); } diff --git a/apps/server/src/server/api/routers/user.ts b/apps/server/src/server/api/routers/user.ts index 471629fe1..2bb47f540 100644 --- a/apps/server/src/server/api/routers/user.ts +++ b/apps/server/src/server/api/routers/user.ts @@ -2,7 +2,6 @@ import { TRPCError } from '@trpc/server'; import { updateUserSchema } from '../zodSchemas/user.schema'; import { createTRPCRouter, protectedProcedure } from '../trpc'; import { returnUser, handleNewUser } from '../../../utils/routers/user'; -import { User } from '@prisma/client'; import { sendAccountDeletionCancellationEmail, sendSoftDeletionEmail } from '../../../utils/notification/userEmails'; import { ensureAdmin } from '../../../utils/routers/trpc' @@ -23,7 +22,7 @@ export const userRouter = createTRPCRouter({ if (ctx.isAdmin === true) { // If impersonatedUser is null, login the admin themself if (ctx.isImpersonatedUser === false) { - const adminUser = ctx.user as User + const adminUser = ctx.user return { status: 'success', data: adminUser, @@ -31,14 +30,14 @@ export const userRouter = createTRPCRouter({ } // Here, impersonatedUser is true, so admin is trying to crud the user data. // Don't undo soft delete if user is softdeleted. - const user = ctx.user as User + const user = ctx.user return { status: 'success', data: user, } } // Since authorized client is not admin, do normal login concept - const user = ctx.user as User + const user = ctx.user // If user is deleted, send account deletion cancellation email if (user.deletedAt) { await sendAccountDeletionCancellationEmail(user); diff --git a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts index a681f8f1d..a4742830e 100644 --- a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts +++ b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts @@ -1,25 +1,17 @@ import { z } from "zod"; -const GeoEventProviderConfigSchema = z.object({ - apiUrl: z.string(), - mapKey: z.string(), - sourceKey: z.string(), -}); - // Zod Schema for createGeoEventProvider export const createGeoEventProviderSchema = z.object({ - type: z.enum(["fire"]), - isActive: z.boolean(), - providerKey: z.enum(["FIRMS"]), - config: GeoEventProviderConfigSchema, + isActive: z.boolean().optional(), + name: z.string(), + description: z.string().optional(), }); // Zod Schema for updateGeoEventProvider body const UpdateGeoEventProviderBodySchema = z.object({ - type: z.enum(["fire"]), - isActive: z.boolean(), - providerKey: z.enum(["FIRMS"]), - config: GeoEventProviderConfigSchema, + isActive: z.boolean().optional(), + name: z.string().optional(), + description: z.string().optional(), }).partial(); // Zod Schema for updateGeoEventProvider params @@ -30,5 +22,5 @@ export const geoEventProviderParamsSchema = z.object({ // Zod Schema for updateGeoEventProvider export const updateGeoEventProviderSchema = z.object({ params: geoEventProviderParamsSchema, - body: UpdateGeoEventProviderBodySchema, + body: UpdateGeoEventProviderBodySchema.optional(), }); \ No newline at end of file From 8b7da406bf0bc858242b8058b83e10bc3b3caa8b Mon Sep 17 00:00:00 2001 From: Sagar Date: Wed, 21 Jun 2023 02:30:04 +0200 Subject: [PATCH 03/11] add plan concept to user table eg: basic | member | developer --- .../20230620222147_user_in_geoprovider/migration.sql | 3 +++ apps/server/prisma/schema.prisma | 1 + 2 files changed, 4 insertions(+) diff --git a/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql b/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql index d6ba5fc8c..24bd1fc9a 100644 --- a/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql +++ b/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql @@ -2,5 +2,8 @@ ALTER TABLE "GeoEventProvider" ADD COLUMN "userId" TEXT; +ALTER TABLE "User" +ADD COLUMN "plan" TEXT NOT NULL DEFAULT 'basic'; + -- AddForeignKey ALTER TABLE "GeoEventProvider" ADD CONSTRAINT "GeoEventProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 7c82a2fc8..c936b757f 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -22,6 +22,7 @@ model User { isVerified Boolean? lastLogin DateTime? signupDate DateTime @default(now()) + plan String @default("basic") roles Role @default(ROLE_CLIENT) alertMethods AlertMethod[] projects Project[] From a90e0396f76c1741f1ed8678455f9c6cba423b30 Mon Sep 17 00:00:00 2001 From: Sagar Date: Thu, 22 Jun 2023 00:29:42 +0200 Subject: [PATCH 04/11] Add route to add geoEvents draft --- .../server/src/server/api/routers/geoEvent.ts | 80 +++++++++++++++++++ .../server/api/zodSchemas/geoEvent.schema.ts | 18 +++++ 2 files changed, 98 insertions(+) create mode 100644 apps/server/src/server/api/routers/geoEvent.ts create mode 100644 apps/server/src/server/api/zodSchemas/geoEvent.schema.ts diff --git a/apps/server/src/server/api/routers/geoEvent.ts b/apps/server/src/server/api/routers/geoEvent.ts new file mode 100644 index 000000000..f58ec76ff --- /dev/null +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -0,0 +1,80 @@ +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createGeoEventSchema } from "../zodSchemas/geoEvent.schema"; +import { prisma } from "../../../../src/server/db"; +import { type GeoEventProvider } from "@prisma/client"; + +export const geoEventRouter = createTRPCRouter({ + + create: protectedProcedure + .input(createGeoEventSchema) + // Here user should be able to authenticate with either accesstoken or using the GeoEventProvider Api Key + // x-client-id should be passed in the header regardless of authentication method + + .mutation(async ({ ctx, input }) => { + try { + const { type, latitude, longitude, eventDate, geometry, confidence, radius, data} = input; + const geoEventProviderClientId = ctx.req.headers["x-client-id"]; + const geoEventProviderApiKey = ctx.req.headers["x-api-key"]; + + //Authentication ensure user is authenticated either with access token or with GeoEventProviderApiKey + if (!geoEventProviderApiKey && !user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `Missing Authorization header`, + }); + } + + // get geoeventprovider from database where clientId = geoEventProviderClientId + // and (apiKey = geoEventProviderApiKey or userId = user.id) + // if no geoeventprovider is found throw error + // if geoeventprovider is found and apiKey is not equal to geoEventProviderApiKey + // throw error + // if geoeventprovider is found and userId is not equal to user.id continue normally + // if geoeventprovider is found and userId is equal to user.id continue normally + + const provider: GeoEventProvider = prisma.geoEventProvider.findFirst({ + where: { + AND: [ + { clientId: geoEventProviderClientId }, + { + OR: [ + { clientApiKey: geoEventProviderApiKey }, + { userId: user.id } + ] + } + ] + } + }); + + + + + + if (!geoEventProviderClientId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `Missing x-client-id header`, + }); + } + + + // To ensure same data isn't stored twice we will use id as a unique identifier + // generated from a hash of latitude, longitude, eventDate, type and x-client-id + // This will allow us to store the same event multiple times if it is from different providers + // but will not store the same event twice from the same provider + + // identify in which slice the geoEvent belongs to + // const slice = await getSlice({ latitude, longitude }); + + + } + catch (error) { + console.log(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `${error}`, + }); + } + }), +}); diff --git a/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts new file mode 100644 index 000000000..7e54701a4 --- /dev/null +++ b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +// Zod Schema for createGeoEvent +export const createGeoEventSchema = z.object({ + type: z.string(), + latitude: z.number(), + longitude: z.number(), + eventDate: z.date(), + geometry: z.object({ + type: z.string(), + coordinates: z.array(z.number()), + }), + confidence: z.number(), + radius: z.number(), + data: z.object({ + [z.string()]: z.any(), + }), +}); From 492c8fba3c75a5036b29a409992562956e865b47 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Fri, 7 Jul 2023 09:12:16 +0545 Subject: [PATCH 05/11] User Plan Logic Fix error messages PlanetROs have pro plan by default Limit site and alertMethod creation for basic and pro users --- .../src/server/api/routers/alertMethod.ts | 27 +++++++++++++++++++ apps/server/src/server/api/routers/site.ts | 16 +++++++++++ apps/server/src/utils/routers/user.ts | 13 +++++++-- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/apps/server/src/server/api/routers/alertMethod.ts b/apps/server/src/server/api/routers/alertMethod.ts index fa91409af..d8fe23509 100644 --- a/apps/server/src/server/api/routers/alertMethod.ts +++ b/apps/server/src/server/api/routers/alertMethod.ts @@ -106,6 +106,33 @@ export const alertMethodRouter = createTRPCRouter({ .input(createAlertMethodSchema) .mutation(async ({ ctx, input }) => { const userId = ctx.user!.id; + const userPlan = ctx.user!.plan + // Setup user plan constraint + if (userPlan === 'basic' || userPlan === 'pro') { + const alertMethodCount = await ctx.prisma.alertMethod.count({ + where: { + userId: userId, + }, + }); + // Basic Plan + if(userPlan === 'basic'){ + if (alertMethodCount >= 5) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please upgrade your plan to continue adding more alert methods.", + }); + } + } + // Pro plan + if(userPlan === 'pro'){ + if (alertMethodCount >= 20) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please upgrade your plan to continue adding more alert methods.", + }); + } + } + } //Check if that AlertMethod already exists const existingAlertMethod = await ctx.prisma.alertMethod.findFirst({ where: { diff --git a/apps/server/src/server/api/routers/site.ts b/apps/server/src/server/api/routers/site.ts index a072d8cb5..55450bf3d 100644 --- a/apps/server/src/server/api/routers/site.ts +++ b/apps/server/src/server/api/routers/site.ts @@ -13,6 +13,22 @@ export const siteRouter = createTRPCRouter({ .input(createSiteSchema) .mutation(async ({ ctx, input }) => { const userId = ctx.user!.id; + const userPlan = ctx.user!.plan + // Setup user plan constraint + if (userPlan === 'basic') { + const siteCount = await ctx.prisma.site.count({ + where: { + userId: userId, + }, + }); + // Basic plan + if (siteCount >= 5) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please upgrade your plan to continue adding more alert methods.", + }); + } + } try { const radius = input.radius ?? 0; const origin = 'firealert'; diff --git a/apps/server/src/utils/routers/user.ts b/apps/server/src/utils/routers/user.ts index e45e46737..37db0698e 100644 --- a/apps/server/src/utils/routers/user.ts +++ b/apps/server/src/utils/routers/user.ts @@ -19,6 +19,15 @@ interface CreateUserArgs { export async function createUserInPrismaTransaction({ id, prisma, sub, name, email, emailVerified, image, isPlanetRO, remoteId }: CreateUserArgs) { const detectionMethods: ('MODIS' | 'VIIRS' | 'LANDSAT' | 'GEOSTATIONARY')[] = ["MODIS", "VIIRS", "LANDSAT"] + + // Normal user plan must be "basic" by default + let userPlan: string = "basic" + + //PlanetRO user plan must be "pro" by default + if(isPlanetRO){ + userPlan = "pro" + } + const createdUser = await prisma.user.create({ data: { id: id ? id : undefined, @@ -30,7 +39,8 @@ export async function createUserInPrismaTransaction({ id, prisma, sub, name, ema emailVerified: emailVerified, lastLogin: new Date(), detectionMethods: detectionMethods, - remoteId: remoteId + remoteId: remoteId, + plan: userPlan, }, }); return createdUser; @@ -92,7 +102,6 @@ export async function handleNewUser(bearer_token: string) { const planetId = getPlanetUser.id // Create FireAlert User - const createdUser: User = await createUserInPrismaTransaction({ prisma, sub, name: name, image: picture, email, emailVerified: email_verified, isPlanetRO: isPlanetRO, remoteId: planetId }) const createdAlertMethod = await createAlertMethodInPrismaTransaction({ prisma, email, isVerified: email_verified, method: "email", isEnabled: true, userId: createdUser.id }) if (isPlanetRO) { From 86541fec8702e64649607d82e9b8b628fa61f8d4 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Sun, 9 Jul 2023 21:40:14 +0545 Subject: [PATCH 06/11] geoEvent and geoEventProvider routers get geoEventProvider fixes geoEventProvider router createGeoEvent procedure --- .../server/src/server/api/routers/geoEvent.ts | 135 ++++++++++++++---- .../server/api/routers/geoEventProvider.ts | 61 ++++++-- .../server/api/zodSchemas/geoEvent.schema.ts | 12 +- .../api/zodSchemas/geoEventProvider.schema.ts | 8 +- apps/server/src/utils/routers/geoEvent.ts | 21 +++ 5 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 apps/server/src/utils/routers/geoEvent.ts diff --git a/apps/server/src/server/api/routers/geoEvent.ts b/apps/server/src/server/api/routers/geoEvent.ts index f58ec76ff..afd6b525b 100644 --- a/apps/server/src/server/api/routers/geoEvent.ts +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createGeoEventSchema } from "../zodSchemas/geoEvent.schema"; -import { prisma } from "../../../../src/server/db"; -import { type GeoEventProvider } from "@prisma/client"; +import { GeoEventProvider, Prisma } from "@prisma/client"; +import { createXXHash3 } from "hash-wasm"; +import { getSlice } from "../../../utils/routers/geoEvent"; export const geoEventRouter = createTRPCRouter({ @@ -10,67 +11,139 @@ export const geoEventRouter = createTRPCRouter({ .input(createGeoEventSchema) // Here user should be able to authenticate with either accesstoken or using the GeoEventProvider Api Key // x-client-id should be passed in the header regardless of authentication method - + .mutation(async ({ ctx, input }) => { try { - const { type, latitude, longitude, eventDate, geometry, confidence, radius, data} = input; + const { type, latitude, longitude, eventDate: inputEventDate, ...rest } = input; const geoEventProviderClientId = ctx.req.headers["x-client-id"]; - const geoEventProviderApiKey = ctx.req.headers["x-api-key"]; + const geoEventProviderClientApiKey = ctx.req.headers["x-api-key"]; //Authentication ensure user is authenticated either with access token or with GeoEventProviderApiKey - if (!geoEventProviderApiKey && !user) { + if (!geoEventProviderClientApiKey && !ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED", message: `Missing Authorization header`, }); } - // get geoeventprovider from database where clientId = geoEventProviderClientId - // and (apiKey = geoEventProviderApiKey or userId = user.id) + if (!geoEventProviderClientId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `Missing x-client-id header`, + }); + } + + if (typeof geoEventProviderClientId !== 'string') { + throw new TRPCError({ code: "BAD_REQUEST", message: `The value of req.headers['x-client-id'] must be a string` }); + } + + // get geoeventprovider from database where clientId = geoEventProviderClientId and (apiKey = geoEventProviderApiKey or userId = user.id) // if no geoeventprovider is found throw error - // if geoeventprovider is found and apiKey is not equal to geoEventProviderApiKey + // if geoeventprovider is found and apiKey is not equal to geoEventProviderClientApiKey // throw error // if geoeventprovider is found and userId is not equal to user.id continue normally // if geoeventprovider is found and userId is equal to user.id continue normally - - const provider: GeoEventProvider = prisma.geoEventProvider.findFirst({ - where: { - AND: [ - { clientId: geoEventProviderClientId }, - { - OR: [ - { clientApiKey: geoEventProviderApiKey }, - { userId: user.id } - ] - } - ] - } - }); - - - - if (!geoEventProviderClientId) { + // Aashish: The code below replicates above commented logic. + let provider: (GeoEventProvider | null) = null; + + // If apiKey exists and is a string + if (geoEventProviderClientApiKey && typeof geoEventProviderClientApiKey === 'string') { + // Find provider where clientId and apiKey + provider = await ctx.prisma.geoEventProvider.findFirst({ + where: { + clientId: geoEventProviderClientId, + clientApiKey: geoEventProviderClientApiKey, + }, + }); + // If provider exists, and provider's clientApiKey is not equal to the apiKey from headers + if (provider && provider.clientApiKey !== geoEventProviderClientApiKey) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Client API Key does not match", + }); + } + } else if (ctx.user?.id) { + // Find provider where clientId and userId + provider = await ctx.prisma.geoEventProvider.findFirst({ + where: { + clientId: geoEventProviderClientId, + userId: ctx.user?.id, + }, + }); + } + + if (!provider) { throw new TRPCError({ - code: "UNAUTHORIZED", - message: `Missing x-client-id header`, + code: "NOT_FOUND", + message: `Provider not found`, }); } + + // To ensure same data isn't stored twice we will use id as a unique identifier // generated from a hash of latitude, longitude, eventDate, type and x-client-id // This will allow us to store the same event multiple times if it is from different providers // but will not store the same event twice from the same provider - + + // Create checksum + const hasher = await createXXHash3(); + hasher.init(); // Reset the hasher + const eventDate = inputEventDate ? inputEventDate : new Date() + const checksum = hasher.update( + latitude.toString() + + longitude.toString() + + eventDate.toISOString() + + type + + geoEventProviderClientId + ).digest('hex'); + + // Verify if the event already exists + const existingEvent = await ctx.prisma.geoEvent.findUnique({ where: { id: checksum } }); + + // If the event already exists, send a success message saying the creation process was cancelled + // Because the event was already stored in our database. + if (existingEvent) { + return { + status: 'success', + message: 'Cancelled. This event was already present in the database.' + } + } // identify in which slice the geoEvent belongs to - // const slice = await getSlice({ latitude, longitude }); + const slice = getSlice(latitude); + // Create GeoEvent + const geoEvent = await ctx.prisma.geoEvent.create({ + data: { + type, + latitude, + longitude, + eventDate, + ...rest, + geoEventProviderId: provider.id, + slice, + geoEventProviderClientId, + }, + }); + // Our database trigger functions automatically creates a geometry column that is a postgis hash + // made out of latitude and longitude values + // Return success message with the created geoEvent + return { + status: 'success', + data: geoEvent, + }; } catch (error) { console.log(error); + if (error instanceof TRPCError) { + // if the error is already a TRPCError, just re-throw it + throw error; + } + // if it's a different type of error, throw a new TRPCError throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `${error}`, diff --git a/apps/server/src/server/api/routers/geoEventProvider.ts b/apps/server/src/server/api/routers/geoEventProvider.ts index 188bbff98..fc89ed8cb 100644 --- a/apps/server/src/server/api/routers/geoEventProvider.ts +++ b/apps/server/src/server/api/routers/geoEventProvider.ts @@ -9,11 +9,11 @@ import { type TRPCContext } from "../../../../src/Interfaces/Context"; // Users -export function checkUserOwnsProvider (ctx: TRPCContext, id: string) { +export function checkUserOwnsProvider(ctx: TRPCContext, id: string) { return ctx.prisma.geoEventProvider.findFirst({ where: { id: id, - userId: ctx.user.id, + userId: ctx.user!.id, } }); } @@ -25,13 +25,13 @@ export const geoEventProviderRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { try { const { name, description, isActive } = input; - const userId = ctx.user.id ?? null; + const userId = ctx.user!.id ?? null; const geoEventProvider = await ctx.prisma.geoEventProvider.create({ data: { name, description, type: "fire", - isActive, + isActive: isActive ? isActive : false , clientApiKey: randomUUID(), clientId: randomUUID(), fetchFrequency: null, @@ -66,7 +66,7 @@ export const geoEventProviderRouter = createTRPCRouter({ try { const id = input.params.id; const body = input.body; - const userId = ctx.user.id; + const userId = ctx.user!.id; //check if user owns the geoEventProvider const geoEventProvider = await checkUserOwnsProvider(ctx, id); if (!geoEventProvider) { @@ -75,7 +75,6 @@ export const geoEventProviderRouter = createTRPCRouter({ message: "GeoEventProvider with that id does not exist", }); } - const updatedGeoEventProvider = await ctx.prisma.geoEventProvider.update({ where: { id: id, @@ -91,12 +90,39 @@ export const geoEventProviderRouter = createTRPCRouter({ clientId: true, } }); - return updatedGeoEventProvider; + } catch (error) { + if (error instanceof TRPCError) { + // if the error is already a TRPCError, just re-throw it + throw error; + } + // if it's a different type of error, throw a new TRPCError + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `${error}`, + }); + } + }), + + get: protectedProcedure + .input(geoEventProviderParamsSchema) + .query(async ({ ctx, input }) => { + try { + const geoEventProvider = await checkUserOwnsProvider(ctx, input.id); + if (!geoEventProvider) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "GeoEventProvider with that id does not exist", + }); + } + return { + status: "success", + data: geoEventProvider, + }; } catch (error) { console.log(error); throw new TRPCError({ - code: `${error.code}}`, + code: "CONFLICT", message: `${error}`, }); } @@ -108,7 +134,7 @@ export const geoEventProviderRouter = createTRPCRouter({ const geoEventProviders = await ctx.prisma.geoEventProvider.findMany( { where: { - userId: ctx.user.id, + userId: ctx.user!.id, }, select: { id: true, @@ -121,12 +147,15 @@ export const geoEventProviderRouter = createTRPCRouter({ } } ); - return geoEventProviders; } catch (error) { - console.log(error); + if (error instanceof TRPCError) { + // if the error is already a TRPCError, just re-throw it + throw error; + } + // if it's a different type of error, throw a new TRPCError throw new TRPCError({ - code: `${error.code}}`, + code: "INTERNAL_SERVER_ERROR", message: `${error}`, }); } @@ -199,9 +228,13 @@ export const geoEventProviderRouter = createTRPCRouter({ message: `GeoEventProvider with id ${deletedGeoEventProvider.id} has been deleted.`, }; } catch (error) { - console.log(error); + if (error instanceof TRPCError) { + // if the error is already a TRPCError, just re-throw it + throw error; + } + // if it's a different type of error, throw a new TRPCError throw new TRPCError({ - code: `${error.code}`, + code: "INTERNAL_SERVER_ERROR", message: `${error}`, }); } diff --git a/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts index 7e54701a4..445f49f54 100644 --- a/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts +++ b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts @@ -6,13 +6,7 @@ export const createGeoEventSchema = z.object({ latitude: z.number(), longitude: z.number(), eventDate: z.date(), - geometry: z.object({ - type: z.string(), - coordinates: z.array(z.number()), - }), - confidence: z.number(), - radius: z.number(), - data: z.object({ - [z.string()]: z.any(), - }), + confidence: z.enum(["high","medium","low"]), + radius: z.number().optional(), + data: z.record(z.unknown()).optional(), }); diff --git a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts index a4742830e..4caef46fb 100644 --- a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts +++ b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts @@ -9,9 +9,9 @@ export const createGeoEventProviderSchema = z.object({ // Zod Schema for updateGeoEventProvider body const UpdateGeoEventProviderBodySchema = z.object({ - isActive: z.boolean().optional(), - name: z.string().optional(), - description: z.string().optional(), + isActive: z.boolean(), + name: z.string(), + description: z.string(), }).partial(); // Zod Schema for updateGeoEventProvider params @@ -22,5 +22,5 @@ export const geoEventProviderParamsSchema = z.object({ // Zod Schema for updateGeoEventProvider export const updateGeoEventProviderSchema = z.object({ params: geoEventProviderParamsSchema, - body: UpdateGeoEventProviderBodySchema.optional(), + body: UpdateGeoEventProviderBodySchema, }); \ No newline at end of file diff --git a/apps/server/src/utils/routers/geoEvent.ts b/apps/server/src/utils/routers/geoEvent.ts new file mode 100644 index 000000000..c611b984f --- /dev/null +++ b/apps/server/src/utils/routers/geoEvent.ts @@ -0,0 +1,21 @@ +export function getSlice(latitude: number) { + if(latitude >= -90 && latitude < -30) { + return '1'; + } else if (latitude >= -30 && latitude < -15) { + return '2'; + } else if (latitude >= -15 && latitude < 0) { + return '3'; + } else if (latitude >= 0 && latitude < 15) { + return '4'; + } else if (latitude >= 15 && latitude < 30) { + return '5'; + } else if (latitude >= 30 && latitude < 45) { + return '6'; + } else if (latitude >= 45 && latitude < 60) { + return '7'; + } else if (latitude >= 60 && latitude <= 90) { + return '8'; + } else { + return '0'; + } +} From bfa720ec5743d3b9d3e7d9b8cac4cb91e020ebcb Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Wed, 12 Jul 2023 21:16:37 +0545 Subject: [PATCH 07/11] Clean code --- apps/server/src/server/api/routers/geoEvent.ts | 2 +- apps/server/src/server/api/routers/geoEventProvider.ts | 1 - apps/server/src/server/api/routers/site.ts | 2 +- apps/server/src/utils/routers/user.ts | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/server/src/server/api/routers/geoEvent.ts b/apps/server/src/server/api/routers/geoEvent.ts index afd6b525b..42298d226 100644 --- a/apps/server/src/server/api/routers/geoEvent.ts +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -1,7 +1,7 @@ import { TRPCError } from "@trpc/server"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createGeoEventSchema } from "../zodSchemas/geoEvent.schema"; -import { GeoEventProvider, Prisma } from "@prisma/client"; +import { GeoEventProvider } from "@prisma/client"; import { createXXHash3 } from "hash-wasm"; import { getSlice } from "../../../utils/routers/geoEvent"; diff --git a/apps/server/src/server/api/routers/geoEventProvider.ts b/apps/server/src/server/api/routers/geoEventProvider.ts index fc89ed8cb..e4f1f7aac 100644 --- a/apps/server/src/server/api/routers/geoEventProvider.ts +++ b/apps/server/src/server/api/routers/geoEventProvider.ts @@ -66,7 +66,6 @@ export const geoEventProviderRouter = createTRPCRouter({ try { const id = input.params.id; const body = input.body; - const userId = ctx.user!.id; //check if user owns the geoEventProvider const geoEventProvider = await checkUserOwnsProvider(ctx, id); if (!geoEventProvider) { diff --git a/apps/server/src/server/api/routers/site.ts b/apps/server/src/server/api/routers/site.ts index 55450bf3d..2e49a8562 100644 --- a/apps/server/src/server/api/routers/site.ts +++ b/apps/server/src/server/api/routers/site.ts @@ -262,7 +262,7 @@ export const siteRouter = createTRPCRouter({ }); } try { - let updatedData = input.body + const updatedData = input.body // Initialize data let data: Prisma.SiteUpdateInput = updatedData; // If Site is associated with PlanetRO User then don't allow changes on fields other than radius and isMonitored diff --git a/apps/server/src/utils/routers/user.ts b/apps/server/src/utils/routers/user.ts index 37db0698e..20cf01443 100644 --- a/apps/server/src/utils/routers/user.ts +++ b/apps/server/src/utils/routers/user.ts @@ -95,7 +95,7 @@ export async function handleNewUser(bearer_token: string) { // Auth0 has a bug where email_verified is a sometimes string instead of a boolean // Therefore check both string and boolean values - const email_verified = userData.email_verified === "true" || true ? true : false + const email_verified = userData.email_verified === "true"; const getPlanetUser = await planetUser(bearer_token) const isPlanetRO = getPlanetUser.isPlanetRO @@ -103,7 +103,7 @@ export async function handleNewUser(bearer_token: string) { // Create FireAlert User const createdUser: User = await createUserInPrismaTransaction({ prisma, sub, name: name, image: picture, email, emailVerified: email_verified, isPlanetRO: isPlanetRO, remoteId: planetId }) - const createdAlertMethod = await createAlertMethodInPrismaTransaction({ prisma, email, isVerified: email_verified, method: "email", isEnabled: true, userId: createdUser.id }) + await createAlertMethodInPrismaTransaction({ prisma, email, isVerified: email_verified, method: "email", isEnabled: true, userId: createdUser.id }) if (isPlanetRO) { const projects = await fetchProjectsWithSitesForUser(bearer_token) if (projects.length > 0) { From e1d2f6c47e4d8d037be7f181a41563af8c867d75 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Tue, 18 Jul 2023 18:26:57 +0545 Subject: [PATCH 08/11] RO Users should have pro plan --- apps/server/src/utils/routers/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/utils/routers/user.ts b/apps/server/src/utils/routers/user.ts index 20cf01443..48d538d0a 100644 --- a/apps/server/src/utils/routers/user.ts +++ b/apps/server/src/utils/routers/user.ts @@ -24,7 +24,7 @@ export async function createUserInPrismaTransaction({ id, prisma, sub, name, ema let userPlan: string = "basic" //PlanetRO user plan must be "pro" by default - if(isPlanetRO){ + if(isPlanetRO === true){ userPlan = "pro" } From 7af46bbdcd1404a6789a307aa59535f7b65dd850 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Thu, 17 Aug 2023 20:04:39 +0545 Subject: [PATCH 09/11] create siteAlert procedure --- apps/server/src/server/api/routers/alert.ts | 143 +++++++++++++++++- .../server/src/server/api/routers/geoEvent.ts | 11 +- .../src/server/api/zodSchemas/alert.schema.ts | 12 +- 3 files changed, 156 insertions(+), 10 deletions(-) diff --git a/apps/server/src/server/api/routers/alert.ts b/apps/server/src/server/api/routers/alert.ts index 288f0c849..83470a91a 100644 --- a/apps/server/src/server/api/routers/alert.ts +++ b/apps/server/src/server/api/routers/alert.ts @@ -1,11 +1,13 @@ import { TRPCError } from "@trpc/server"; -import { queryAlertSchema } from '../zodSchemas/alert.schema' +import { queryAlertSchema, createAlertSchema } from '../zodSchemas/alert.schema' import { createTRPCRouter, protectedProcedure, publicProcedure, } from "../trpc"; import { getLocalTime, subtractDays } from "../../../utils/date"; +import { GeoEventProvider } from "@prisma/client"; +import { createXXHash3 } from "hash-wasm"; export const alertRouter = createTRPCRouter({ @@ -141,4 +143,143 @@ export const alertRouter = createTRPCRouter({ }); } }), + + create: protectedProcedure + .input(createAlertSchema) + .mutation(async ({ ctx, input }) => { + try { + const { + siteId, + type, + latitude, + longitude, + eventDate: inputEventDate, + detectedBy: geoEventProviderClientId, + confidence, + ...rest + } = input; + const geoEventProviderClientApiKey = ctx.req.headers["x-api-key"]; + + // Ensure the user is authenticated + //Authentication ensure user is authenticated either with access token or with GeoEventProviderApiKey + if (!geoEventProviderClientApiKey && !ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `Missing Authorization header`, + }); + } + + if (!geoEventProviderClientId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `Missing Provider Client Id Authorization`, + }); + } + + // Check whether the User is a GeoEventProviderClient or if the request has a GeoEventProviderApiKey and GeoEventProviderClientId + // Logic: + // get geoeventprovider from database where clientId = geoEventProviderClientId and (apiKey = geoEventProviderApiKey or userId = user.id) + // if no geoeventprovider is found throw error + // This logic ensures that either a geoEventProviderClient can continue, or that the one accessing this route must have a correct geoEventProviderClientKey + + let provider: (GeoEventProvider | null) = null; + + // If apiKey exists and is a string + if (geoEventProviderClientApiKey && typeof geoEventProviderClientApiKey === 'string') { + // Find provider where clientId and apiKey + provider = await ctx.prisma.geoEventProvider.findFirst({ + where: { + clientId: geoEventProviderClientId, + clientApiKey: geoEventProviderClientApiKey, + }, + }); + } else if (ctx.user?.id) { + // Find provider where clientId and userId + provider = await ctx.prisma.geoEventProvider.findFirst({ + where: { + clientId: geoEventProviderClientId, + userId: ctx.user?.id, + }, + }); + } + + if (!provider) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Provider not found`, + }); + } + + // Get site from the database using siteId; if not found, throw an error + const site = await ctx.prisma.site.findUnique({ where: { id: siteId } }); + if (!site) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Site Not Found`, + }); + } + + // To ensure same data isn't stored twice we will use id as a unique identifier + // generated from a hash of latitude, longitude, eventDate, type and x-client-id + // This will allow us to store the same event multiple times if it is from different providers + // but will not store the same event twice from the same provider + + // Create checksum + const hasher = await createXXHash3(); + hasher.init(); // Reset the hasher + const eventDate = inputEventDate ? inputEventDate : new Date() + const checksum = hasher.update( + latitude.toString() + + longitude.toString() + + eventDate.toISOString() + + type + + geoEventProviderClientId + ).digest('hex'); + + // Verify if the event already exists + const existingSiteAlert = await ctx.prisma.siteAlert.findUnique({ where: { id: checksum } }); + + // If the event already exists, send a success message saying the creation process was cancelled + // Because the event was already stored in our database. + if (existingSiteAlert) { + return { + status: 'success', + message: 'Cancelled. This alert was already present in the database.' + } + } + + // Create SiteAlert + const siteAlert = await ctx.prisma.siteAlert.create({ + data: { + siteId, + type, + latitude, + longitude, + eventDate: eventDate, + detectedBy: geoEventProviderClientId, + confidence, + ...rest, + isProcessed: false, + }, + }); + + // Return success message with the created siteAlert + return { + status: 'success', + data: siteAlert, + }; + } + catch (error) { + console.log(error); + if (error instanceof TRPCError) { + // If the error is already a TRPCError, just re-throw it + throw error; + } + // If it's a different type of error, throw a new TRPCError + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `${error}`, + }); + } + }), }); diff --git a/apps/server/src/server/api/routers/geoEvent.ts b/apps/server/src/server/api/routers/geoEvent.ts index 42298d226..006a367c8 100644 --- a/apps/server/src/server/api/routers/geoEvent.ts +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -37,15 +37,13 @@ export const geoEventRouter = createTRPCRouter({ throw new TRPCError({ code: "BAD_REQUEST", message: `The value of req.headers['x-client-id'] must be a string` }); } + // Check whether the User is a GeoEventProviderClient or if the request has a GeoEventProviderApiKey and GeoEventProviderClientId + // Logic: // get geoeventprovider from database where clientId = geoEventProviderClientId and (apiKey = geoEventProviderApiKey or userId = user.id) // if no geoeventprovider is found throw error - // if geoeventprovider is found and apiKey is not equal to geoEventProviderClientApiKey - // throw error - // if geoeventprovider is found and userId is not equal to user.id continue normally - // if geoeventprovider is found and userId is equal to user.id continue normally + // This logic ensures that either a geoEventProviderClient can continue, or that the one accessing this route must have a correct geoEventProviderClientKey - // Aashish: The code below replicates above commented logic. let provider: (GeoEventProvider | null) = null; // If apiKey exists and is a string @@ -81,9 +79,6 @@ export const geoEventRouter = createTRPCRouter({ }); } - - - // To ensure same data isn't stored twice we will use id as a unique identifier // generated from a hash of latitude, longitude, eventDate, type and x-client-id // This will allow us to store the same event multiple times if it is from different providers diff --git a/apps/server/src/server/api/zodSchemas/alert.schema.ts b/apps/server/src/server/api/zodSchemas/alert.schema.ts index fe3f79281..4d17afd09 100644 --- a/apps/server/src/server/api/zodSchemas/alert.schema.ts +++ b/apps/server/src/server/api/zodSchemas/alert.schema.ts @@ -4,7 +4,17 @@ export const queryAlertSchema = z.object({ id: z.string(), }) - +export const createAlertSchema = z.object({ + siteId: z.string(), + type: z.enum(["fire"]), + latitude: z.number(), + longitude: z.number(), + eventDate: z.date().optional(), + detectedBy: z.string(), + confidence: z.enum(["medium", "low", "high"]), + distance: z.number().optional(), + data: z.record(z.unknown()).optional(), +}); From 1847b833bdb7e8aa18fc77c03deb32721d8913d8 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Thu, 17 Aug 2023 20:38:34 +0545 Subject: [PATCH 10/11] corrections in create siteAlert and protections from XSS --- apps/server/src/server/api/routers/alert.ts | 5 +++-- .../server/src/server/api/routers/geoEvent.ts | 12 +++--------- .../src/server/api/zodSchemas/alert.schema.ts | 15 ++++++++++++--- .../api/zodSchemas/geoEventProvider.schema.ts | 19 ++++++++++++++----- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/server/src/server/api/routers/alert.ts b/apps/server/src/server/api/routers/alert.ts index c283ecbbd..3f58ce5dd 100644 --- a/apps/server/src/server/api/routers/alert.ts +++ b/apps/server/src/server/api/routers/alert.ts @@ -236,11 +236,12 @@ export const alertRouter = createTRPCRouter({ // Create checksum const hasher = await createXXHash3(); hasher.init(); // Reset the hasher - const eventDate = inputEventDate ? inputEventDate : new Date() + const eventDate = inputEventDate ? inputEventDate : new Date(); + const eventDayIsoString = eventDate.toISOString().split('T')[0]; // Extracting the date portion (YYYY-MM-DD); const checksum = hasher.update( latitude.toString() + longitude.toString() + - eventDate.toISOString() + + eventDayIsoString + type + geoEventProviderClientId ).digest('hex'); diff --git a/apps/server/src/server/api/routers/geoEvent.ts b/apps/server/src/server/api/routers/geoEvent.ts index 006a367c8..aafa33386 100644 --- a/apps/server/src/server/api/routers/geoEvent.ts +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -55,13 +55,6 @@ export const geoEventRouter = createTRPCRouter({ clientApiKey: geoEventProviderClientApiKey, }, }); - // If provider exists, and provider's clientApiKey is not equal to the apiKey from headers - if (provider && provider.clientApiKey !== geoEventProviderClientApiKey) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Client API Key does not match", - }); - } } else if (ctx.user?.id) { // Find provider where clientId and userId provider = await ctx.prisma.geoEventProvider.findFirst({ @@ -87,11 +80,12 @@ export const geoEventRouter = createTRPCRouter({ // Create checksum const hasher = await createXXHash3(); hasher.init(); // Reset the hasher - const eventDate = inputEventDate ? inputEventDate : new Date() + const eventDate = inputEventDate ? inputEventDate : new Date(); + const eventDayIsoString = eventDate.toISOString().split('T')[0]; // Extracting the date portion (YYYY-MM-DD); const checksum = hasher.update( latitude.toString() + longitude.toString() + - eventDate.toISOString() + + eventDayIsoString + type + geoEventProviderClientId ).digest('hex'); diff --git a/apps/server/src/server/api/zodSchemas/alert.schema.ts b/apps/server/src/server/api/zodSchemas/alert.schema.ts index 96816cb6a..ed19c9f4d 100644 --- a/apps/server/src/server/api/zodSchemas/alert.schema.ts +++ b/apps/server/src/server/api/zodSchemas/alert.schema.ts @@ -1,19 +1,28 @@ import {z} from 'zod'; +import validator from 'validator'; + +export const detectedBySchema = z.string().min(5, { message: "DetectedBy must be 5 or more characters long" }).max(100, { message: "DetectedBy be 100 or less characters long" }).refine(value => { + const sanitized = validator.escape(value); + return sanitized === value; +}, { + message: 'DetectedBy contains invalid characters', +}); export const queryAlertSchema = z.object({ id: z.string().cuid({ message: "Invalid CUID" }), }) export const createAlertSchema = z.object({ - siteId: z.string(), + siteId: z.string().cuid({ message: "Invalid CUID" }), type: z.enum(["fire"]), latitude: z.number(), longitude: z.number(), eventDate: z.date().optional(), - detectedBy: z.string(), + detectedBy: detectedBySchema, confidence: z.enum(["medium", "low", "high"]), distance: z.number().optional(), - data: z.record(z.unknown()).optional(), + // TODO: Do we need the data field here? This could lead to security vulnerabilities + // data: z.record(z.unknown()).optional(), }); diff --git a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts index 4caef46fb..22f3af6fc 100644 --- a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts +++ b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts @@ -1,22 +1,31 @@ import { z } from "zod"; +import { nameSchema } from "./user.schema"; +import validator from 'validator'; + +export const descriptionSchema = z.string().min(5, { message: "Description must be 5 or more characters long" }).max(1000, { message: "Description be 1000 or less characters long" }).refine(value => { + const sanitized = validator.escape(value); + return sanitized === value; +}, { +message: 'Description contains invalid characters', +}); // Zod Schema for createGeoEventProvider export const createGeoEventProviderSchema = z.object({ isActive: z.boolean().optional(), - name: z.string(), - description: z.string().optional(), + name: nameSchema, + description: descriptionSchema.optional(), }); // Zod Schema for updateGeoEventProvider body const UpdateGeoEventProviderBodySchema = z.object({ isActive: z.boolean(), - name: z.string(), - description: z.string(), + name: nameSchema, + description: descriptionSchema, }).partial(); // Zod Schema for updateGeoEventProvider params export const geoEventProviderParamsSchema = z.object({ - id: z.string(), + id: z.string().cuid({ message: "Invalid CUID" }), }); // Zod Schema for updateGeoEventProvider From da9ce99a3a356e4760372c0267ec6a0046f4ff19 Mon Sep 17 00:00:00 2001 From: Aashish Dhakal <85501584+dhakalaashish@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:11:13 +0545 Subject: [PATCH 11/11] update prisma schema, isApproved and authorization checks --- apps/server/prisma/schema.prisma | 1 + apps/server/src/server/api/routers/alert.ts | 30 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index c936b757f..485c93038 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -102,6 +102,7 @@ model GeoEventProvider { clientId String // LANDSAT_NRT fetchFrequency Int? isActive Boolean + isApproved Boolean lastRun DateTime? config Json userId String? diff --git a/apps/server/src/server/api/routers/alert.ts b/apps/server/src/server/api/routers/alert.ts index 3f58ce5dd..ae5458724 100644 --- a/apps/server/src/server/api/routers/alert.ts +++ b/apps/server/src/server/api/routers/alert.ts @@ -152,7 +152,9 @@ export const alertRouter = createTRPCRouter({ }); } }), - + + // TODO: Make sure that the siteId must belong to the clientApiKey! + // TODO: We need to check if the geoEventProvider is verified or enabled or not! create: protectedProcedure .input(createAlertSchema) .mutation(async ({ ctx, input }) => { @@ -219,12 +221,32 @@ export const alertRouter = createTRPCRouter({ }); } - // Get site from the database using siteId; if not found, throw an error - const site = await ctx.prisma.site.findUnique({ where: { id: siteId } }); + if(!provider.isApproved){ + throw new TRPCError({ + code: "METHOD_NOT_SUPPORTED", + message: `GeoEventProvider is not verified. Verify it first to create alerts.`, + }); + } + + // Find the userId associated with the provider + // Since the provider is either found by using the user's authorization headers, or by using the clientApiKey + // This ensures that, there is no difference between a user accessing their own provider, + // or someone else accessing the provider with the clientApiKey (which acts as a password for the provider) + // Then, we can find the provider.userId for that provider. + const providerUserId = provider.userId ? provider.userId : "" + + // Get site from the database using siteId and providerUserId; if not found, throw an error + const site = await ctx.prisma.site.findUnique({ + where: { + id: siteId, + userId: providerUserId, + } + }); if (!site) { throw new TRPCError({ code: "NOT_FOUND", - message: `Site Not Found`, + message: `Site Not Found.`, + // Either the site does not exist, or not authorized to access that site. }); }