diff --git a/src/modules/creators/creator-holders.controller.ts b/src/modules/creators/creator-holders.controller.ts index bdb91a0..03983bb 100644 --- a/src/modules/creators/creator-holders.controller.ts +++ b/src/modules/creators/creator-holders.controller.ts @@ -12,6 +12,7 @@ import { attachTimestampHeader } from '../../utils/timestamp-headers.utils'; import { parsePublicQuery } from '../../utils/public-query-parse.utils'; import { buildOffsetPaginationMeta } from '../../utils/pagination.utils'; import { handleCreatorParamNotFound } from '../creator/creator.utils'; +import { parseCreatorId } from '../../utils/creator-id.utils'; /** * Controller for GET /api/v1/creators/:id/holders @@ -26,8 +27,8 @@ import { handleCreatorParamNotFound } from '../creator/creator.utils'; */ export const httpGetCreatorHolders: AsyncController = async (req, res, next) => { try { - const rawId = req.params['id']; - const id = typeof rawId === 'string' ? rawId : String(rawId ?? ''); + const rawId = req.params.id; + const creatorId = parseCreatorId(Array.isArray(rawId) ? rawId[0] : rawId); const parsed = parsePublicQuery(CreatorHoldersQuerySchema, req.query, { debugContext: 'creator-holders-query', @@ -37,7 +38,7 @@ export const httpGetCreatorHolders: AsyncController = async (req, res, next) => return sendValidationError(res, 'Invalid query parameters', parsed.details); } - const creator = await findCreatorByIdOrHandle(id); + const creator = await findCreatorByIdOrHandle(String(creatorId)); if (!handleCreatorParamNotFound(res, creator)) return; const [holders, total] = await fetchCreatorHolders(creator.id, parsed.data); diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index 46c87e1..e7f5b11 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -20,6 +20,7 @@ import { incrementFilterParseError, type FilterParseErrorCategory, } from '../../utils/filter-parse-metrics.utils'; +import { parseCreatorId } from '../../utils/creator-id.utils'; /** * Controller for GET /api/v1/creators @@ -101,16 +102,10 @@ function categorizeParseError( */ export const httpGetCreatorStats: AsyncController = async (req, res, next) => { try { - const { id } = req.params; + const rawId = req.params.id; + const _creatorId = parseCreatorId(Array.isArray(rawId) ? rawId[0] : rawId); - // Validate creator ID format (basic validation) - if (!id || typeof id !== 'string') { - return sendValidationError(res, 'Invalid creator ID', [ - { field: 'id', message: 'Creator ID must be a valid string' }, - ]); - } - - // TODO: Fetch actual creator metrics from database/service + // TODO: Fetch actual creator metrics from database/service using _creatorId // For now, return placeholder data const placeholderMetrics = { holderCount: 0, diff --git a/src/modules/webhooks/webhook-signature.middleware.ts b/src/modules/webhooks/webhook-signature.middleware.ts index 1f85f3a..5155494 100644 --- a/src/modules/webhooks/webhook-signature.middleware.ts +++ b/src/modules/webhooks/webhook-signature.middleware.ts @@ -6,6 +6,7 @@ import { ErrorCode } from '../../constants/error.constants'; import { prisma } from '../../utils/prisma.utils'; import { logger } from '../../utils/logger.utils'; import { createHash } from 'crypto'; +import { parseCreatorId } from '../../utils/creator-id.utils'; export interface WalletSignedRequest extends Request { walletAddress?: string; @@ -72,10 +73,12 @@ export function requireWalletSignature() { return; } - const rawCreatorId = req.params.id; - const creatorId = Array.isArray(rawCreatorId) ? rawCreatorId[0] : rawCreatorId; - if (!creatorId) { - sendError(res, 400, ErrorCode.BAD_REQUEST, 'Missing creator ID in path'); + let creatorId: string; + try { + const rawId = req.params.id; + creatorId = String(parseCreatorId(Array.isArray(rawId) ? rawId[0] : rawId)); + } catch { + sendError(res, 400, ErrorCode.BAD_REQUEST, 'Creator ID must be a positive integer'); return; } diff --git a/src/utils/creator-id.utils.ts b/src/utils/creator-id.utils.ts new file mode 100644 index 0000000..f0172f3 --- /dev/null +++ b/src/utils/creator-id.utils.ts @@ -0,0 +1,25 @@ +import { validationError } from '../middlewares/error.middleware'; + +const POSITIVE_INTEGER_RE = /^\d+$/; + +export function parseCreatorId(raw: string): number { + if (!raw || typeof raw !== 'string') { + throw validationError('Creator ID is required'); + } + + const trimmed = raw.trim(); + if (!trimmed) { + throw validationError('Creator ID is required'); + } + + if (!POSITIVE_INTEGER_RE.test(trimmed)) { + throw validationError('Creator ID must be a positive integer'); + } + + const parsed = Number.parseInt(trimmed, 10); + if (parsed <= 0 || !Number.isFinite(parsed)) { + throw validationError('Creator ID must be a positive integer'); + } + + return parsed; +} diff --git a/src/utils/test/creator-id.utils.test.ts b/src/utils/test/creator-id.utils.test.ts new file mode 100644 index 0000000..bb510eb --- /dev/null +++ b/src/utils/test/creator-id.utils.test.ts @@ -0,0 +1,58 @@ +import { parseCreatorId } from '../creator-id.utils'; +import { ApiError } from '../../middlewares/error.middleware'; + +describe('parseCreatorId', () => { + it('parses a valid positive integer string', () => { + expect(parseCreatorId('42')).toBe(42); + expect(parseCreatorId('1')).toBe(1); + expect(parseCreatorId('999999')).toBe(999999); + }); + + it('parses a trimmed integer string', () => { + expect(parseCreatorId(' 42 ')).toBe(42); + }); + + it('throws a 400 error for a float string', () => { + expect(() => parseCreatorId('3.14')).toThrow(ApiError); + expect(() => parseCreatorId('3.14')).toThrow('Creator ID must be a positive integer'); + }); + + it('throws a 400 error for a negative number', () => { + expect(() => parseCreatorId('-5')).toThrow(ApiError); + expect(() => parseCreatorId('-5')).toThrow('Creator ID must be a positive integer'); + }); + + it('throws a 400 error for a non-numeric string', () => { + expect(() => parseCreatorId('abc')).toThrow(ApiError); + expect(() => parseCreatorId('abc')).toThrow('Creator ID must be a positive integer'); + }); + + it('throws a 400 error for an empty string', () => { + expect(() => parseCreatorId('')).toThrow(ApiError); + expect(() => parseCreatorId('')).toThrow('Creator ID is required'); + }); + + it('throws a 400 error for a whitespace-only string', () => { + expect(() => parseCreatorId(' ')).toThrow(ApiError); + expect(() => parseCreatorId(' ')).toThrow('Creator ID is required'); + }); + + it('throws a 400 error for a string with trailing non-digits', () => { + expect(() => parseCreatorId('42abc')).toThrow(ApiError); + expect(() => parseCreatorId('42abc')).toThrow('Creator ID must be a positive integer'); + }); + + it('throws a 400 error for zero', () => { + expect(() => parseCreatorId('0')).toThrow(ApiError); + expect(() => parseCreatorId('0')).toThrow('Creator ID must be a positive integer'); + }); + + it('sets statusCode 400 on the thrown error', () => { + try { + parseCreatorId('not-a-number'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + expect((error as ApiError).statusCode).toBe(400); + } + }); +});