Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export const RESPONSE_MESSAGES = {
INVALID_EMAIL_400: { statusCode: 400, message: 'Invalid email.' },
USER_OUT_OF_EMAIL_CREDITS_400: { statusCode: 400, message: 'User out of email credits.' },
USER_OUT_OF_POST_CREDITS_400: { statusCode: 400, message: 'User out of post credits.' },
NO_INVOICE_FOUND_404: { statusCode: 404, message: 'No invoice found.' }
NO_INVOICE_FOUND_404: { statusCode: 404, message: 'No invoice found.' },
INVALID_FIELDS_FORMAT_400: { statusCode: 400, message: "'fields' must be a valid JSON array." },
INVALID_FIELD_STRUCTURE_400: { statusCode: 400, message: "Each field must have 'name' property as string." },
INVALID_AMOUNT_400: { statusCode: 400, message: "'amount' must be a valid positive number." }
}

export const SOCKET_MESSAGES = {
Expand Down
17 changes: 12 additions & 5 deletions pages/api/payments/paymentId/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Decimal } from '@prisma/client/runtime/library'
import { generatePaymentId } from 'services/clientPaymentService'
import { parseAddress, parseCreatePaymentIdPOSTRequest } from 'utils/validators'
import { RESPONSE_MESSAGES } from 'constants/index'
Expand All @@ -14,11 +13,10 @@ export default async (req: any, res: any): Promise<void> => {
await runMiddleware(req, res, cors)
if (req.method === 'POST') {
try {
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined
const { amount, fields, address } = parseCreatePaymentIdPOSTRequest(req.body)
const parsedAddress = parseAddress(address)

const paymentId = await generatePaymentId(address, amount)
const paymentId = await generatePaymentId(parsedAddress, amount, fields)

res.status(200).json({ paymentId })
} catch (error: any) {
Expand All @@ -29,6 +27,15 @@ export default async (req: any, res: any): Promise<void> => {
case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message:
res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400)
break
case RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message:
res.status(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.statusCode).json(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400)
break
case RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message:
res.status(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.statusCode).json(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400)
break
case RESPONSE_MESSAGES.INVALID_AMOUNT_400.message:
res.status(RESPONSE_MESSAGES.INVALID_AMOUNT_400.statusCode).json(RESPONSE_MESSAGES.INVALID_AMOUNT_400)
break
default:
res.status(500).json({ statusCode: 500, message: error.message })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `ClientPayment` ADD COLUMN `fields` LONGTEXT NOT NULL DEFAULT '[]';
1 change: 1 addition & 0 deletions prisma-local/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ model ClientPayment {
addressString String
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
fields String @db.LongText @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
34 changes: 32 additions & 2 deletions services/clientPaymentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { parseAddress } from 'utils/validators'
import { addressExists } from './addressService'
import moment from 'moment'

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
export interface ClientPaymentField {
name: string
text?: string
type?: string
value?: string | boolean
}

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal, fields?: ClientPaymentField[]): Promise<string> => {
const rawUUID = uuidv4()
const cleanUUID = rawUUID.replace(/-/g, '')
const status = 'PENDING' as ClientPaymentStatus
Expand All @@ -29,7 +36,8 @@ export const generatePaymentId = async (address: string, amount?: Prisma.Decimal
},
paymentId: cleanUUID,
status,
amount
amount,
fields: fields !== undefined ? JSON.stringify(fields) : '[]'
},
include: {
address: true
Expand Down Expand Up @@ -57,6 +65,28 @@ export const getClientPayment = async (paymentId: string): Promise<Prisma.Client
})
}

export const getClientPaymentFields = async (paymentId: string): Promise<ClientPaymentField[]> => {
const clientPayment = await prisma.clientPayment.findUnique({
where: { paymentId },
select: { fields: true }
})
if (clientPayment === null) {
return []
}
try {
return JSON.parse(clientPayment.fields) as ClientPaymentField[]
} catch {
return []
}
}

export const updateClientPaymentFields = async (paymentId: string, fields: ClientPaymentField[]): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { fields: JSON.stringify(fields) }
})
}

export const cleanupExpiredClientPayments = async (): Promise<void> => {
const cutoff = moment.utc().subtract(CLIENT_PAYMENT_EXPIRATION_TIME, 'milliseconds').toDate()

Expand Down
69 changes: 67 additions & 2 deletions utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import crypto from 'crypto'
import { getUserPrivateKey } from '../services/userService'
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import moment from 'moment-timezone'
import { ClientPaymentField } from 'services/clientPaymentService'

/* The functions exported here should validate the data structure / syntax of an
* input by throwing an error in case something is different from the expected.
Expand Down Expand Up @@ -574,10 +575,70 @@ export interface CreateInvoicePOSTParameters {
export interface CreatePaymentIdPOSTParameters {
address?: string
amount?: string
fields?: string
}
export interface ClientPaymentFieldInput {
name?: string
text?: string
type?: string
value?: string | boolean
}

export interface CreatePaymentIdInput {
address: string
amount?: string
amount?: Prisma.Decimal
fields?: ClientPaymentField[]
}

export const parseClientPaymentFields = function (fieldsInput: string | undefined): ClientPaymentField[] | undefined {
if (fieldsInput === undefined || fieldsInput === '') {
return undefined
}

let parsedFields: unknown
try {
parsedFields = JSON.parse(fieldsInput)
} catch {
throw new Error(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message)
}

if (!Array.isArray(parsedFields)) {
throw new Error(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message)
}

for (const field of parsedFields) {
if (
typeof field !== 'object' ||
field === null ||
typeof field.name !== 'string' ||
field.name?.trim() === ''
) {
throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message)
}
if (field.type !== undefined && typeof field.type !== 'string') {
throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message)
}
if (field.value !== undefined && typeof field.value !== 'string' && typeof field.value !== 'boolean') {
throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message)
}
}

return parsedFields as ClientPaymentField[]
}
Comment on lines +593 to +627
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

ast-grep --pattern 'interface ClientPaymentField {
  $$$
}'

Repository: PayButton/paybutton-server

Length of output: 399


🏁 Script executed:

# Also search with rg in case the interface is defined differently
rg 'interface ClientPaymentField' -A 10

Repository: PayButton/paybutton-server

Length of output: 1265


🏁 Script executed:

# Check for type definitions as well
rg 'type ClientPaymentField' -A 10

Repository: PayButton/paybutton-server

Length of output: 52


🏁 Script executed:

rg 'field\.text' -B 2 -A 2

Repository: PayButton/paybutton-server

Length of output: 52


🏁 Script executed:

# Check if text is accessed without type checking anywhere
rg '\.text' services/clientPaymentService.ts -B 2 -A 2

Repository: PayButton/paybutton-server

Length of output: 52


🏁 Script executed:

# Find where parseClientPaymentFields is used
rg 'parseClientPaymentFields' -B 2 -A 2

Repository: PayButton/paybutton-server

Length of output: 586


🏁 Script executed:

# Search for "text" in the context of fields to understand its role
rg 'text.*field|field.*text' -i

Repository: PayButton/paybutton-server

Length of output: 699


🏁 Script executed:

# Check the broader context of field properties usage
rg 'field\.(name|type|value|text)' -B 1 -A 1

Repository: PayButton/paybutton-server

Length of output: 1475


Add validation for the optional text property to be consistent with type and value validation.

The ClientPaymentField interface includes an optional text: string property, but the validation function checks the types of type and value when present while skipping validation for text. Add a type check for text similar to the existing conditional checks:

if (field.text !== undefined && typeof field.text !== 'string') {
  throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message)
}
🤖 Prompt for AI Agents
In utils/validators.ts around lines 593 to 627, the parser validates optional
fields `type` and `value` but omits validation for the optional `text` property;
add a check that if field.text is defined it must be a string and throw
RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message on violation, mirroring
the existing conditional style used for `type` and `value`.


export const parseAmount = function (amountInput: string | undefined): Prisma.Decimal | undefined {
if (amountInput === undefined || amountInput === '') {
return undefined
}

const trimmedAmount = amountInput.trim()
const numericAmount = Number(trimmedAmount)

if (isNaN(numericAmount) || numericAmount <= 0) {
throw new Error(RESPONSE_MESSAGES.INVALID_AMOUNT_400.message)
}

return new Prisma.Decimal(trimmedAmount)
}

export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentIdPOSTParameters): CreatePaymentIdInput {
Expand All @@ -588,8 +649,12 @@ export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentId
throw new Error(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message)
}

const amount = parseAmount(params.amount)
const fields = parseClientPaymentFields(params.fields)

return {
address: params.address,
amount: params.amount === '' ? undefined : params.amount
amount,
fields
}
}