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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ DATABASE_URL="postgresql://user:password@localhost:5432/lookups?schema=public"
AUTH_SECRET="mysecret"

# A JSON array string of valid token issuers.
VALID_ISSUERS='["https://topcoder-dev.auth0.com/","https://api.topcoder.com"]'
VALID_ISSUERS='["https://topcoder-dev.auth0.com/","https://auth.topcoder-dev.com/","https://topcoder.auth0.com/","https://auth.topcoder.com/","https://api.topcoder.com","https://api.topcoder-dev.com"]'

## Running the Application

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"pg": "^8.16.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.2",
"tc-core-library-js": "github:appirio-tech/tc-core-library-js#security",
"tc-core-library-js": "github:topcoder-platform/tc-core-library-js#master",
"@nestjs/schematics": "^11.0.9",
"@types/jest": "^29.5.8",
"@nestjs/testing": "^11.1.9"
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sql/reports/sfdc/payments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ WHERE
AND ($8::numeric IS NULL OR p.total_amount >= $8::numeric)
AND ($9::numeric IS NULL OR p.total_amount <= $9::numeric)
AND ($10::text[] IS NULL OR c.status::text = ANY($10::text[]))
AND ($11::text[] IS NULL OR p.payment_status::text = ANY($11::text[]))
ORDER BY p.created_at DESC
14 changes: 12 additions & 2 deletions sql/reports/topcoder/30-day-payments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,25 @@ recent_payments AS (
WHERE w.type = 'PAYMENT'
AND p.installment_number = 1
AND p.payment_status = 'PAID'
AND COALESCE(p.date_paid, p.created_at) >= (CURRENT_DATE - INTERVAL '3 months')
AND p.created_at >= (CURRENT_DATE - INTERVAL '3 months')
)
SELECT
cl."name" AS customer,
cl."codeName" AS client_codename,
COALESCE(
NULLIF(TRIM(proj.details::jsonb #>> '{taasDefinition,oppurtunityDetails,customerName}'), ''),

Choose a reason for hiding this comment

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

[❗❗ correctness]
The JSON path '{taasDefinition,oppurtunityDetails,customerName}' contains a typo in oppurtunityDetails. It should likely be opportunityDetails. This could lead to incorrect data retrieval if the JSON structure is not as expected.

NULLIF(TRIM(proj.details::jsonb #>> '{project_data,group_customer_name}'), ''),
ba."name"
) AS customer_name,
COALESCE(c."projectId"::text, ba."projectId") AS project_id,
proj.name AS project_name,
ba.id::text AS billing_account_id,
ba."name" AS billing_account_name,
COALESCE(
NULLIF(TRIM(proj.details::jsonb #>> '{taasDefinition,oppurtunityDetails,customerName}'), ''),

Choose a reason for hiding this comment

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

[❗❗ correctness]
The JSON path '{taasDefinition,oppurtunityDetails,customerName}' contains a typo in oppurtunityDetails. It should likely be opportunityDetails. This could lead to incorrect data retrieval if the JSON structure is not as expected.

NULLIF(TRIM(proj.details::jsonb #>> '{project_data,group_customer_name}'), ''),
ba."name"
) AS customer_name,

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The field customer_name is being selected twice in the query. This could lead to confusion or errors in the result set. Consider removing the duplicate selection.

rp.challenge_id,
c."name" AS challenge_name,
c."createdAt" AS challenge_created_at,
Expand Down Expand Up @@ -73,4 +83,4 @@ LEFT JOIN projects.projects proj
ON proj.id = c."projectId"::bigint
LEFT JOIN members.member mem
ON mem."userId"::text = rp.winner_id
ORDER BY payment_created_at DESC;
ORDER BY payment_created_at DESC;
30 changes: 29 additions & 1 deletion src/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@ import {
Injectable,
NestMiddleware,
UnauthorizedException,
Logger,
} from "@nestjs/common";
import { Response, NextFunction } from "express";
import { ConfigService } from "@nestjs/config";
import { middleware } from "tc-core-library-js";
const { jwtAuthenticator: authenticator } = middleware;

const logger = new Logger("AuthMiddleware");

function decodeTokenPayload(token: string): Record<string, unknown> | null {

Choose a reason for hiding this comment

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

[❗❗ security]
The decodeTokenPayload function manually decodes the JWT payload without verifying the token's signature. This could lead to security issues if the payload is trusted without verification. Consider using a library like jsonwebtoken to decode and verify the token securely.

try {
const parts = token.split(".");
if (parts.length < 2) {
return null;
}
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded =
payload + "=".repeat((4 - (payload.length % 4)) % 4);
const decoded = Buffer.from(padded, "base64").toString("utf8");
return JSON.parse(decoded);
} catch {
return null;
}
}

@Injectable()
export class AuthMiddleware implements NestMiddleware {
private jwtAuthenticator;
Expand All @@ -19,7 +38,7 @@ export class AuthMiddleware implements NestMiddleware {
);
let issuersValue = this.configService.get<string>(
"VALID_ISSUERS",
'["https://api.topcoder.com","https://topcoder-dev.auth0.com/"]',
'["https://api.topcoder.com","https://api.topcoder-dev.com","https://topcoder-dev.auth0.com/","https://auth.topcoder-dev.com/","https://topcoder.auth0.com/","https://auth.topcoder.com/"]',
);

// The tc-core-library-js authenticator expects a string that is a valid JSON array.
Expand All @@ -39,6 +58,15 @@ export class AuthMiddleware implements NestMiddleware {
if (req.headers.authorization) {
this.jwtAuthenticator(req, res, (err) => {
if (err) {
const token = req.headers.authorization?.replace(/^Bearer\s+/i, "");
const payload = token ? decodeTokenPayload(token) : null;
logger.warn({

Choose a reason for hiding this comment

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

[❗❗ security]
Logging the decoded JWT payload, even partially, can expose sensitive information. Ensure that sensitive data is not logged or consider redacting sensitive fields before logging.

message: "JWT authentication failed",
error: err?.message,
tokenIss: payload?.["iss"],
tokenAud: payload?.["aud"],
validIssuers: this.configService.get<string>("VALID_ISSUERS"),
});
return next(new UnauthorizedException(err.message));
}
next();
Expand Down
9 changes: 9 additions & 0 deletions src/reports/report-directory.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ const maxPaymentParam: ReportParameter = {
location: "query",
};

const paymentStatusParam: ReportParameter = {
name: "status",
type: "string[]",
description:
"Payment status codes to filter by (for example ON_HOLD, PROCESSING, CANCELLED). Leave blank to include all statuses.",
location: "query",
};

const paymentsFilters = [
billingAccountIdsParam,
challengeNameParam,
Expand All @@ -160,6 +168,7 @@ const paymentsFilters = [
handlesParam,
minPaymentParam,
maxPaymentParam,
paymentStatusParam,
];

const baFeesDateParams: ReportParameter[] = [
Expand Down
2 changes: 2 additions & 0 deletions src/reports/sfdc/sfdc-reports.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ describe("SfdcReportsController", () => {
challengeIds: "e74c3e37-73c9-474e-a838-a38dd4738906",
handles: "user_01",
challengeStatus: "COMPLETED",
status: "ON_HOLD",
});

await controller.getPaymentsReport(dto);
Expand All @@ -180,6 +181,7 @@ describe("SfdcReportsController", () => {
challengeIds: ["e74c3e37-73c9-474e-a838-a38dd4738906"],
handles: ["user_01"],
challengeStatus: ["COMPLETED"],
status: ["ON_HOLD"],
}),
);
});
Expand Down
22 changes: 22 additions & 0 deletions src/reports/sfdc/sfdc-reports.dto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ describe("PaymentsReportQueryDto validation", () => {
minPaymentAmount: 100,
maxPaymentAmount: 1000,
challengeStatus: ["COMPLETED", "ACTIVE"],
status: ["ON_HOLD", "PROCESSING"],
});
expect(errors).toHaveLength(0);
});
Expand Down Expand Up @@ -388,6 +389,27 @@ describe("PaymentsReportQueryDto validation", () => {
expect(errors).toHaveLength(0);
expect(dto.challengeStatus).toEqual(["COMPLETED"]);
});

it("accepts payment status values", async () => {
const { errors } = await validatePaymentDto({
status: ["ON_HOLD", "PROCESSING"],
});
expect(errors).toHaveLength(0);
});

it("rejects empty payment status entries", async () => {
const { errors } = await validatePaymentDto({ status: [""] });
expect(errors.some((err) => err.property === "status")).toBe(true);
});

it("transforms single payment status into array", async () => {
const { dto, errors } = await validatePaymentDto({
// @ts-expect-error intentional single value for transform check
status: "PROCESSING",
});
expect(errors).toHaveLength(0);
expect(dto.status).toEqual(["PROCESSING"]);
});
});

describe("TaasJobsReportQueryDto validation", () => {
Expand Down
12 changes: 12 additions & 0 deletions src/reports/sfdc/sfdc-reports.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@ export class PaymentsReportQueryDto {
@IsNotEmpty({ each: true })
@Transform(transformArray)
challengeStatus?: string[];

@ApiProperty({
required: false,
description:
"List of payment statuses to filter payments (for example ON_HOLD, PROCESSING, CANCELLED). If omitted, all statuses are included.",
example: ["ON_HOLD", "PROCESSING"],
})
@IsOptional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@Transform(transformArray)
status?: string[];
}

export class PaymentsReportResponse {
Expand Down
25 changes: 25 additions & 0 deletions src/reports/sfdc/sfdc-reports.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
undefined,
undefined,
]);
expect(result).toEqual(normalizedPaymentData);
});
Expand All @@ -336,6 +337,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
undefined,
undefined,
]);
});

Expand All @@ -353,6 +355,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
100,
500,
["COMPLETED"],
["PROCESSING"],
]);
});

Expand All @@ -370,6 +373,25 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
["COMPLETED"],
undefined,
]);
});

it("handles payment status filter", async () => {
await service.getPaymentsReport(mockPaymentQueryDto.paymentStatus);

expect(mockDbService.query).toHaveBeenCalledWith(mockSqlQuery, [
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
["ON_HOLD"],
]);
});

Expand All @@ -387,6 +409,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
[null as unknown as string],
undefined,
]);
});

Expand Down Expand Up @@ -426,6 +449,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
undefined,
undefined,
]);
});

Expand All @@ -443,6 +467,7 @@ describe("SfdcReportsService - getPaymentsReport", () => {
undefined,
undefined,
undefined,
undefined,
]);
});
});
Expand Down
1 change: 1 addition & 0 deletions src/reports/sfdc/sfdc-reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class SfdcReportsService {
filters.minPaymentAmount,
filters.maxPaymentAmount,
filters.challengeStatus,
filters.status,
]);

this.logger.debug("Mapped payments to the final report format");
Expand Down
4 changes: 4 additions & 0 deletions src/reports/sfdc/test-helpers/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export const mockPaymentQueryDto: Record<string, PaymentsReportQueryDto> = {
nullChallengeStatus: {
challengeStatus: [null as unknown as string],
},
paymentStatus: {
status: ["ON_HOLD"],
},
full: {
billingAccountIds: ["80001012", "!90000000"],
challengeIds: ["e74c3e37-73c9-474e-a838-a38dd4738906"],
Expand All @@ -152,6 +155,7 @@ export const mockPaymentQueryDto: Record<string, PaymentsReportQueryDto> = {
minPaymentAmount: 100,
maxPaymentAmount: 500,
challengeStatus: ["COMPLETED"],
status: ["PROCESSING"],
},
};

Expand Down
Loading