Skip to content
Closed
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,119 changes: 4,746 additions & 2,373 deletions backend/package-lock.json

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "quicklendx-backend",
"version": "0.1.0",
"description": "QuickLendX backend API controllers (TypeScript).",
"description": "",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -19,12 +19,12 @@
"migrate:dry-run": "npm run migrate -- --dry-run",
"migrate:validate": "npm run migrate -- --validate-only",
"migrate:up": "npm run migrate",
"migrate:down": "ts-node src/lib/migrations/cli.ts down"
"migrate:down": "ts-node src/lib/migrations/cli.ts down",
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/helmet": "^0.0.48",
"@types/nodemailer": "^8.0.0",
Expand All @@ -42,8 +42,13 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/node": "^20.19.0",
"@vitest/coverage-v8": "^2.1.9",
"fast-check": "^3.23.2",
"jest": "^29.7.0",
"js-yaml": "^4.3.2",
"supertest": "^6.3.4",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
}
Expand Down
19 changes: 11 additions & 8 deletions backend/src/__mocks__/pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@
*
* All Pool methods return resolved Promises so they behave safely even if
* test code accidentally calls them.
*
* Note: uses plain functions instead of jest.fn() because jest is not
* installed as a dependency in this package.
*/

const mockRelease = jest.fn();
const mockQuery = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });
const mockConnect = jest.fn().mockResolvedValue({
query: mockQuery,
release: mockRelease,
const mockReleaseFn = () => undefined;
const mockQueryFn = () => Promise.resolve({ rows: [], rowCount: 0 });
const mockConnectFn = () => Promise.resolve({
query: mockQueryFn,
release: mockReleaseFn,
});

export class Pool {
connect = mockConnect;
query = mockQuery;
end = jest.fn().mockResolvedValue(undefined);
connect = mockConnectFn;
query = mockQueryFn;
end = () => Promise.resolve(undefined);
}

export default { Pool };
2 changes: 1 addition & 1 deletion backend/src/config/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const isAllowedBrowserOrigin = (origin?: string): boolean => {
};

export const browserCorsOptions: CorsOptions = {
origin: (origin, callback) => {
origin: (origin: any, callback: any) => {
if (isAllowedBrowserOrigin(origin)) {
callback(null, true);
return;
Expand Down
37 changes: 20 additions & 17 deletions backend/src/config/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,6 @@ export function isValidScope(scope: string): boolean {
return SCOPE_REGISTRY.some(s => s.scope === scope);
}

/**
* Validate an array of scopes
*/
export function validateScopes(scopes: string[]): { valid: boolean; invalid: string[] } {
if (!Array.isArray(scopes)) {
return { valid: false, invalid: [] };
}
const invalid = scopes.filter(scope => !isValidScope(scope));
return {
valid: invalid.length === 0,
invalid,
};
}

/**
* Validate an array of scopes against the registry.
*
Expand All @@ -214,6 +200,21 @@ export function validateScopes(scopes: string[]): { valid: boolean; invalid: str
* - Non-array input is a programming error and throws a TypeError naming
* the received type (never a misleading "valid" result).
*/

/** Result shape for scope validation. */
export interface ScopeValidationResult {
valid: boolean;
invalid: readonly string[];
}

/** Describe a value's type for error messages. */
function describeValueType(value: unknown): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (Array.isArray(value)) return "array";
return typeof value;
}

export function validateScopes(scopes: string[]): ScopeValidationResult {
if (!Array.isArray(scopes)) {
throw new TypeError(
Expand Down Expand Up @@ -292,6 +293,8 @@ export function hasRequiredScopes(grantedScopes: string[], requiredScopes: strin
return false;
}

const granted = new Set(grantedScopes);

// Check for admin:* which grants everything
if (grantedScopes.includes('admin:*')) {
return true;
Expand All @@ -304,14 +307,14 @@ export function hasRequiredScopes(grantedScopes: string[], requiredScopes: strin
const [category] = required.split(':');

// Check for exact match
if (granted.has(requiredScope)) {
if (granted.has(required)) {
continue;
}

// Check for wildcard match (e.g., read:* covers read:users). The category
// is derived from the already-validated requirement, so indexOf is safe.
const category = requiredScope.slice(0, requiredScope.indexOf(':'));
if (granted.has(`${category}:*`)) {
const wildcardCategory = required.slice(0, required.indexOf(':'));
if (granted.has(`${wildcardCategory}:*`)) {
continue;
}

Expand Down
10 changes: 1 addition & 9 deletions backend/src/lib/logging/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,7 @@ export interface PolicyFieldEntry {
* 2. `getPolicyFields(fields: string[]): PolicyFieldEntry[]`
* Classifies each field in `fields` returning `{ field, tier }` objects.
*/
export function getPolicyFieldsForTier(tier: FieldTier): string[] {
if (tier !== FieldTier.PUBLIC && tier !== FieldTier.PRIVATE && tier !== FieldTier.SECRET) {
return [];
}

export function getPolicyFieldsForTier(arg: FieldTier | string[]): string[] | { field: string; tier: FieldTier }[] {
if (Array.isArray(arg)) {
for (const elem of arg) {
if (typeof elem !== "string") {
Expand All @@ -185,10 +181,6 @@ export function getPolicyFieldsForTier(tier: FieldTier): string[] {
}));
}

if (typeof arg !== "string") {
throw new TypeError("getPolicyFields: expected a string[] or FieldTier");
}

if (
arg !== FieldTier.PUBLIC &&
arg !== FieldTier.PRIVATE &&
Expand Down
Loading
Loading