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..24bd1fc9a --- /dev/null +++ b/apps/server/prisma/migrations/20230620222147_user_in_geoprovider/migration.sql @@ -0,0 +1,9 @@ +-- Step 1: Add the "userId" column to the "GeoEventProvider" table +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 fd92121b9..439b53e4e 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -10,11 +10,11 @@ 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? plan String @default("basic") //"basic" or "custom" @@ -22,12 +22,14 @@ model User { deletedAt DateTime? isVerified Boolean? lastLogin DateTime? - signupDate DateTime @default(now()) - roles Role @default(ROLE_CLIENT) + signupDate DateTime @default(now()) + plan String @default("basic") + roles Role @default(ROLE_CLIENT) alertMethods AlertMethod[] projects Project[] sites Site[] remoteId String? + GeoEventProvider GeoEventProvider[] } model VerificationRequest { @@ -104,8 +106,11 @@ model GeoEventProvider { clientId String // LANDSAT_NRT fetchFrequency Int? isActive Boolean + isApproved Boolean lastRun DateTime? config Json + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) } model GeoEvent { diff --git a/apps/server/src/server/api/routers/alert.ts b/apps/server/src/server/api/routers/alert.ts index f3dd1c8d9..ae5458724 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({ @@ -150,4 +152,166 @@ 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 }) => { + 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`, + }); + } + + 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.`, + // Either the site does not exist, or not authorized to access that site. + }); + } + + // 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 eventDayIsoString = eventDate.toISOString().split('T')[0]; // Extracting the date portion (YYYY-MM-DD); + const checksum = hasher.update( + latitude.toString() + + longitude.toString() + + eventDayIsoString + + 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 new file mode 100644 index 000000000..aafa33386 --- /dev/null +++ b/apps/server/src/server/api/routers/geoEvent.ts @@ -0,0 +1,142 @@ +import { TRPCError } from "@trpc/server"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createGeoEventSchema } from "../zodSchemas/geoEvent.schema"; +import { GeoEventProvider } from "@prisma/client"; +import { createXXHash3 } from "hash-wasm"; +import { getSlice } from "../../../utils/routers/geoEvent"; + +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: inputEventDate, ...rest } = input; + const geoEventProviderClientId = ctx.req.headers["x-client-id"]; + const geoEventProviderClientApiKey = ctx.req.headers["x-api-key"]; + + //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 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` }); + } + + // 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`, + }); + } + + // 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 eventDayIsoString = eventDate.toISOString().split('T')[0]; // Extracting the date portion (YYYY-MM-DD); + const checksum = hasher.update( + latitude.toString() + + longitude.toString() + + eventDayIsoString + + 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 = 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 5f03c374a..e4f1f7aac 100644 --- a/apps/server/src/server/api/routers/geoEventProvider.ts +++ b/apps/server/src/server/api/routers/geoEventProvider.ts @@ -4,179 +4,237 @@ 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, - isActive, - providerKey, - config, + name, + description, + type: "fire", + isActive: isActive ? isActive : false , + 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); - 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: `Something Went Wrong`, + message: `${error}`, }); } }), - 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; + //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); 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: "CONFLICT", - message: `Something Went Wrong.`, + code: "INTERNAL_SERVER_ERROR", + message: `${error}`, }); } }), - getGeoEventProviders: protectedProcedure - .query(async ({ ctx }) => { - ensureAdmin(ctx) + get: protectedProcedure + .input(geoEventProviderParamsSchema) + .query(async ({ ctx, input }) => { try { - const geoEventProviders = await ctx.prisma.geoEventProvider.findMany(); - + 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: geoEventProviders, + data: geoEventProvider, }; } catch (error) { console.log(error); + throw new TRPCError({ + code: "CONFLICT", + message: `${error}`, + }); + } + }), + + list: protectedProcedure + .query(async ({ ctx }) => { + try { + 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 geoEventProviders; + } 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: "CONFLICT", - message: `Something Went Wrong`, + code: "INTERNAL_SERVER_ERROR", + 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); - 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: "CONFLICT", - message: `Something Went Wrong`, + message: `${error}`, }); } }), - 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.`, }; } 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: "CONFLICT", - message: `Something Went Wrong`, + code: "INTERNAL_SERVER_ERROR", + message: `${error}`, }); } }), diff --git a/apps/server/src/server/api/routers/user.ts b/apps/server/src/server/api/routers/user.ts index 1777c7a90..25ab0a817 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/alert.schema.ts b/apps/server/src/server/api/zodSchemas/alert.schema.ts index efb1a5b7d..e7437d333 100644 --- a/apps/server/src/server/api/zodSchemas/alert.schema.ts +++ b/apps/server/src/server/api/zodSchemas/alert.schema.ts @@ -1,4 +1,12 @@ 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', +}); import validator from 'validator'; @@ -13,7 +21,18 @@ export const queryAlertSchema = z.object({ id: idSchema, }) - +export const createAlertSchema = z.object({ + siteId: z.string().cuid({ message: "Invalid CUID" }), + type: z.enum(["fire"]), + latitude: z.number(), + longitude: z.number(), + eventDate: z.date().optional(), + detectedBy: detectedBySchema, + confidence: z.enum(["medium", "low", "high"]), + distance: z.number().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/geoEvent.schema.ts b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts new file mode 100644 index 000000000..445f49f54 --- /dev/null +++ b/apps/server/src/server/api/zodSchemas/geoEvent.schema.ts @@ -0,0 +1,12 @@ +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(), + 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 ad3fe6b7d..22f3af6fc 100644 --- a/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts +++ b/apps/server/src/server/api/zodSchemas/geoEventProvider.schema.ts @@ -1,33 +1,26 @@ import { z } from "zod"; +import { nameSchema } from "./user.schema"; import validator from 'validator'; -export const sanitizedStringSchema = z.string().refine(value => { +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: 'Contains invalid characters', -}); - -const GeoEventProviderConfigSchema = z.object({ - apiUrl: sanitizedStringSchema, - mapKey: sanitizedStringSchema, - sourceKey: sanitizedStringSchema, +message: 'Description contains invalid characters', }); // 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: nameSchema, + description: descriptionSchema.optional(), }); // Zod Schema for updateGeoEventProvider body const UpdateGeoEventProviderBodySchema = z.object({ - type: z.enum(["fire"]), isActive: z.boolean(), - providerKey: z.enum(["FIRMS"]), - config: GeoEventProviderConfigSchema, + name: nameSchema, + description: descriptionSchema, }).partial(); // Zod Schema for updateGeoEventProvider params 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'; + } +}