From ce9306271d7c29adbc56f709fcaf42289b0781a2 Mon Sep 17 00:00:00 2001
From: nostrdev-com <support@nostrdev.com>
Date: Wed, 2 Apr 2025 11:09:31 +0300
Subject: [PATCH] feat(nostr): added nostrEvent validation

---
 src/models/nostrEvent.ts      |  6 ++--
 src/routes/nostr.router.ts    | 53 +++++++++++++++++++++++++----------
 src/routes/users.router.ts    | 18 ++----------
 src/utils/index.ts            |  1 +
 src/utils/route.ts            | 28 ++++++++++++++++++
 src/utils/validation/index.ts |  1 +
 src/utils/validation/nostr.ts | 24 ++++++++++++++++
 src/utils/validation/user.ts  |  4 +--
 8 files changed, 101 insertions(+), 34 deletions(-)
 create mode 100644 src/utils/route.ts
 create mode 100644 src/utils/validation/nostr.ts

diff --git a/src/models/nostrEvent.ts b/src/models/nostrEvent.ts
index 954f0bf..351ca36 100644
--- a/src/models/nostrEvent.ts
+++ b/src/models/nostrEvent.ts
@@ -1,10 +1,12 @@
+import { ObjectId } from 'mongodb'
 export class NostrEvent {
   constructor(
     public nostrId: string, // nostr unique identifier
     public pubkey: string, // public key of the event creator
     public created_at: number, // timestamp
     public kind: number, // event type, e.g., review, article, comment
-    public tags: [string][], // array of keywords or hashtags
-    public content: string // text content of the event
+    public tags: string[][], // array of keywords or hashtags
+    public content: string, // text content of the event
+    public id?: ObjectId
   ) {}
 }
diff --git a/src/routes/nostr.router.ts b/src/routes/nostr.router.ts
index a9d86e5..3d46cad 100644
--- a/src/routes/nostr.router.ts
+++ b/src/routes/nostr.router.ts
@@ -2,6 +2,13 @@
 import express, { Request, Response } from 'express'
 import { collections } from '../services/database.service'
 import { NostrEvent } from '../models'
+import {
+  nostrEventValidation,
+  handleReqError,
+  handleReqSuccess
+} from '../utils'
+import { Event } from 'nostr-tools'
+import Joi from 'joi'
 
 // Global Config
 export const nostrRouter = express.Router()
@@ -26,23 +33,39 @@ nostrRouter.get('/', async (_req: Request, res: Response) => {
 // POST
 nostrRouter.post('/', async (req: Request, res: Response) => {
   try {
-    const nostrEvent = req.body as NostrEvent
+    const {
+      error,
+      value: event
+    }: { error: Joi.ValidationError | undefined; value: Event } =
+      nostrEventValidation(req.body)
+
+    if (error) {
+      throw error.details[0].message
+    }
+
+    const { id, pubkey, created_at, kind, tags, content } = event
+
+    const events = await collections.nostrEvents
+      ?.find({ nostrId: id })
+      .toArray()
+
+    if (events?.length) {
+      throw new Error('nostr event with provided "id" exists')
+    }
+
+    const nostrEvent: NostrEvent = {
+      nostrId: id,
+      pubkey,
+      created_at,
+      kind,
+      tags,
+      content
+    }
+
     const result = await collections.nostrEvents?.insertOne(nostrEvent)
 
-    if (result) {
-      res
-        .status(201)
-        .send(
-          `Successfully created a new nostrEvent with id ${result.insertedId}`
-        )
-    } else {
-      res.status(500).send('Failed to create a new nostrEvent.')
-    }
+    handleReqSuccess(res, result, 'nostrEvent')
   } catch (error: unknown) {
-    console.error(error)
-
-    if (error instanceof Error) {
-      res.status(400).send(error.message)
-    }
+    handleReqError(res, error)
   }
 })
diff --git a/src/routes/users.router.ts b/src/routes/users.router.ts
index c62a48d..a203b37 100644
--- a/src/routes/users.router.ts
+++ b/src/routes/users.router.ts
@@ -2,7 +2,7 @@
 import express, { Request, Response } from 'express'
 import { collections } from '../services/database.service'
 import { User } from '../models'
-import { userValidation } from '../utils'
+import { userValidation, handleReqError, handleReqSuccess } from '../utils'
 import Joi from 'joi'
 
 // Global Config
@@ -59,20 +59,8 @@ usersRouter.post('/', async (req: Request, res: Response) => {
 
     const result = await collections.users?.insertOne(newUser)
 
-    if (result) {
-      res
-        .status(201)
-        .send(`Successfully created a new user with id ${result.insertedId}`)
-    } else {
-      res.status(500).send('Failed to create a new user.')
-    }
+    handleReqSuccess(res, result, 'user')
   } catch (error) {
-    console.error(error)
-
-    if (error instanceof Error) {
-      res.status(400).send(error.message)
-    } else if (typeof error === 'string') {
-      res.status(400).send(error)
-    }
+    handleReqError(res, error)
   }
 })
diff --git a/src/utils/index.ts b/src/utils/index.ts
index b46597d..086db4e 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,2 +1,3 @@
 export * from './validation'
 export * from './nostr'
+export * from './route'
diff --git a/src/utils/route.ts b/src/utils/route.ts
new file mode 100644
index 0000000..b5a358e
--- /dev/null
+++ b/src/utils/route.ts
@@ -0,0 +1,28 @@
+import { Response } from 'express'
+import { InsertOneResult } from 'mongodb'
+
+export const handleReqError = (res: Response, error: unknown) => {
+  console.error(error)
+
+  if (error instanceof Error) {
+    res.status(400).send(error.message)
+  } else if (typeof error === 'string') {
+    res.status(400).send(error)
+  }
+}
+
+export const handleReqSuccess = (
+  res: Response,
+  result: InsertOneResult<Document> | undefined,
+  itemName: string
+) => {
+  if (result) {
+    res
+      .status(201)
+      .send(
+        `Successfully created a new ${itemName} with id ${result.insertedId}`
+      )
+  } else {
+    res.status(500).send(`Failed to create a new ${itemName}.`)
+  }
+}
diff --git a/src/utils/validation/index.ts b/src/utils/validation/index.ts
index c3a9c65..7a791a3 100644
--- a/src/utils/validation/index.ts
+++ b/src/utils/validation/index.ts
@@ -1 +1,2 @@
 export * from './user'
+export * from './nostr'
diff --git a/src/utils/validation/nostr.ts b/src/utils/validation/nostr.ts
new file mode 100644
index 0000000..3cd9ab9
--- /dev/null
+++ b/src/utils/validation/nostr.ts
@@ -0,0 +1,24 @@
+import Joi from 'joi'
+import { npubToHex, validateHex } from '../nostr'
+
+export const nostrEventValidation = (data: unknown): Joi.ValidationResult =>
+  Joi.object({
+    id: Joi.string().required(),
+    kind: Joi.number().required(),
+    content: Joi.string().required(),
+    tags: Joi.array().items(Joi.array().items(Joi.string())),
+    created_at: Joi.number().required(),
+    pubkey: Joi.string()
+      .custom((value, helper) => {
+        const hex = npubToHex(value as string)
+
+        if (!hex || !validateHex(hex)) {
+          return helper.message({
+            custom: Joi.expression('"pubkey" contains an invalid value')
+          })
+        }
+
+        return value
+      })
+      .required()
+  }).validate(data)
diff --git a/src/utils/validation/user.ts b/src/utils/validation/user.ts
index e022e7c..ddef1f7 100644
--- a/src/utils/validation/user.ts
+++ b/src/utils/validation/user.ts
@@ -16,11 +16,11 @@ const npubValidation = (value: unknown, helper: Joi.CustomHelpers<unknown>) => {
 
 export const userValidation = (data: unknown): Joi.ValidationResult =>
   Joi.object({
-    name: Joi.string().not('').required(),
+    name: Joi.string().required(),
     npub: Joi.alternatives()
       .try(
         Joi.array().items(Joi.string().custom(npubValidation)),
-        Joi.string().not('').custom(npubValidation)
+        Joi.string().custom(npubValidation)
       )
       .required(),
     role: Joi.string()