Skip to content
Merged
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
7 changes: 4 additions & 3 deletions src/modules/creators/creator-holders.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand All @@ -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);
Expand Down
13 changes: 4 additions & 9 deletions src/modules/creators/creators.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions src/modules/webhooks/webhook-signature.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
25 changes: 25 additions & 0 deletions src/utils/creator-id.utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
58 changes: 58 additions & 0 deletions src/utils/test/creator-id.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Loading