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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
**Authorization**
- JWT middleware via `tc-core-library-js` attaches `req.authUser`
- Guards for Roles (e.g., `Administrator`) and M2M Scopes are provided.
- Billing-account management endpoints accept `administrator`, `Talent Manager`,
and `Topcoder Talent Manager` JWT roles; read-only billing-account lookups
also continue to allow `copilot`, `Project Manager`, and `Topcoder Project Manager`.
Comment on lines +27 to +29

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

README claims billing-account management endpoints accept administrator, Talent Manager, and Topcoder Talent Manager JWT roles, but PATCH /billing-accounts/:billingAccountId/lock-amount and PATCH /billing-accounts/:billingAccountId/consume-amount are still restricted to @Roles(ADMIN_ROLE) (and their Swagger docs list only administrator). Either adjust the README wording to exclude these endpoints, or broaden those endpoints’ role checks/docs to match the documented policy.

Suggested change
- Billing-account management endpoints accept `administrator`, `Talent Manager`,
and `Topcoder Talent Manager` JWT roles; read-only billing-account lookups
also continue to allow `copilot`, `Project Manager`, and `Topcoder Project Manager`.
- Most billing-account management endpoints accept `administrator`, `Talent Manager`,
and `Topcoder Talent Manager` JWT roles. The budget mutation endpoints
`PATCH /billing-accounts/:billingAccountId/lock-amount` and
`PATCH /billing-accounts/:billingAccountId/consume-amount` remain
`administrator`-only. Read-only billing-account lookups also continue to
allow `copilot`, `Project Manager`, and `Topcoder Project Manager`.

Copilot uses AI. Check for mistakes.
Project Managers are restricted to billing accounts granted to their own
user id on `GET /billing-accounts` and `GET /billing-accounts/:billingAccountId`.
Role checks are case-insensitive so mixed token casing does not block access.
- Configure env: `AUTH_SECRET` or `AUTH0_URL/AUDIENCE/ISSUER` as needed.

## Quickstart
Expand Down
5 changes: 1 addition & 4 deletions src/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";

// tc-core-library-js is CommonJS only, import via require

const tcCore = require("tc-core-library-js");
import * as tcCore from "tc-core-library-js";

@Injectable()
export class AuthMiddleware implements NestMiddleware {
Expand Down
4 changes: 4 additions & 0 deletions src/auth/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export const SCOPES = {

export const ADMIN_ROLE = "administrator";
export const COPILOT_ROLE = "copilot";
export const PROJECT_MANAGER_ROLE = "Project Manager";
export const TOPCODER_PROJECT_MANAGER_ROLE = "Topcoder Project Manager";
export const TALENT_MANAGER_ROLE = "Talent Manager";
export const TOPCODER_TALENT_MANAGER_ROLE = "Topcoder Talent Manager";
18 changes: 15 additions & 3 deletions src/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class RolesGuard implements CanActivate {
context.getClass(),
]);
if (!required || required.length === 0) return true;
const normalizedRequiredRoles = required.map((role) =>
role.trim().toLowerCase(),
);

const req = context.switchToHttp().getRequest();
const user = req.authUser;
Expand All @@ -31,24 +34,33 @@ export class RolesGuard implements CanActivate {
.split(",")
.map((r: string) => r.trim())
.filter(Boolean);
const normalizedRoles = roles.map((role) => role.toLowerCase());

const ok = roles.some((r: string) => required.includes(r));
const ok = normalizedRoles.some((role: string) =>
normalizedRequiredRoles.includes(role),
);
if (ok) return true;

const fallbackScopes = this.reflector.getAllAndOverride<string[]>(
SCOPES_KEY,
[context.getHandler(), context.getClass()]
[context.getHandler(), context.getClass()],
);

if (fallbackScopes && fallbackScopes.length > 0) {
const normalizedFallbackScopes = fallbackScopes.map((scope) =>
scope.trim().toLowerCase(),
);
const scopes: string[] = Array.isArray(user.scopes)
? user.scopes
: (user.scope || "")
.split(" ")
.map((s: string) => s.trim())
.filter(Boolean);
const normalizedScopes = scopes.map((scope) => scope.toLowerCase());

const scopeOk = fallbackScopes.some((s) => scopes.includes(s));
const scopeOk = normalizedScopes.some((scope) =>
normalizedFallbackScopes.includes(scope),
);
if (scopeOk) return true;
}

Expand Down
8 changes: 4 additions & 4 deletions src/auth/guards/scopes.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export class ScopesGuard implements CanActivate {
const ok = required.some((s) => scopes.includes(s));
if (ok) return true;

const fallbackRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
const fallbackRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);

Comment on lines +38 to 42

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

The scopes fallback-to-roles path in ScopesGuard still performs a case-sensitive role comparison (later in this method) even though RolesGuard now normalizes roles/scopes to be case-insensitive and the README documents case-insensitive role checks. This can cause JWT requests without scopes (common) to pass RolesGuard but fail ScopesGuard if token roles are cased differently (e.g. Administrator). Consider normalizing both fallbackRoles and the extracted roles (trim + lowercase) before comparing, mirroring RolesGuard.

Copilot uses AI. Check for mistakes.
if (fallbackRoles && fallbackRoles.length > 0) {
const roles: string[] = Array.isArray(user.roles)
Expand Down
Loading
Loading