Skip to content

Commit

Permalink
Merge branch 'dev' into rn-1479-pwa-setup
Browse files Browse the repository at this point in the history
  • Loading branch information
tcaiger committed Dec 16, 2024
2 parents bcd82ce + 9cbb126 commit d54a9d2
Show file tree
Hide file tree
Showing 86 changed files with 2,519 additions and 1,396 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,4 @@ In order to automatically format code in VS Code according to our style guide:
1. Install [Prettier for VS Code](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode).
2. Enable the **Editor: Format on Save** setting: `"editor.formatOnSave": true`.

Your files will now be formatted automatically when you save them.
Your files will now be formatted automatically when you save them
6 changes: 4 additions & 2 deletions env/api-client.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ DATA_TABLE_API_URL=
CENTRAL_API_URL=
ENTITY_API_URL=
REPORT_API_URL=
WEB_CONFIG_API_URL=
API_CLIENT_SALT=
WEB_CONFIG_API_URL=
TUPAIA_WEB_SERVER_API_URL=
DATATRAK_WEB_SERVER_API_URL=
ADMIN_PANEL_SERVER_API_URL=
3 changes: 1 addition & 2 deletions env/servers.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

JWT_SECRET=
SESSION_COOKIE_SECRET=


TRUSTED_PROXY_IPS=
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Request } from 'express';
import xlsx from 'xlsx';
import { keyBy } from 'lodash';
import { Route } from '@tupaia/server-boilerplate';

export type ExportEntityHierarchiesRequest = Request<
Expand All @@ -14,6 +15,15 @@ export type ExportEntityHierarchiesRequest = Request<
Record<string, any>
>;

type ExportEntityHierarchiesData = {
parent_name: string;
parent_code: string;
name: string;
code: string;
type: string;
attributes: Record<string, any>;
};

export class ExportEntityHierarchiesRoute extends Route<ExportEntityHierarchiesRequest> {
protected readonly type = 'download';

Expand All @@ -30,18 +40,59 @@ export class ExportEntityHierarchiesRoute extends Route<ExportEntityHierarchiesR
hierarchy,
hierarchy,
{
fields: ['name', 'code', 'parent_code'],
fields: ['parent_code', 'name', 'code', 'type', 'attributes'],
},
false,
true,
false,
);

const descendantsByCode = keyBy(descendants, 'code');

const data = descendants.map((row: ExportEntityHierarchiesData) => {
const record = {
'grandparent name': undefined,
'grandparent code': undefined,
'parent name': undefined,
'parent code': row.parent_code,
name: row.name,
code: row.code,
type: row.type,
attributes: Object.entries(row.attributes)
.map(([key, value]) => `${key}: ${value}`)
.join('\n'),
};

if (row.parent_code) {
const parent = descendantsByCode[row.parent_code];
record['parent name'] = parent?.name;

if (parent?.parent_code) {
const grandparent = descendantsByCode[parent.parent_code];
record['grandparent name'] = grandparent?.name;
record['grandparent code'] = grandparent?.code;
}
}

return record;
});

const projectEntity = await entityApi.getEntity(hierarchy, hierarchy, {
fields: ['name'],
});

const sheetName = projectEntity?.name || hierarchy;
const sheet = xlsx.utils.json_to_sheet(descendants);
const sheet = xlsx.utils.json_to_sheet(data, {
header: [
'grandparent name',
'grandparent code',
'parent name',
'parent code',
'name',
'code',
'type',
'attributes',
],
});
xlsx.utils.book_append_sheet(workbook, sheet, sheetName);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/admin-panel/src/VizBuilderApp/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const DASHBOARD_ITEM_VIZ_TYPES = {
// Matrix
MATRIX: {
name: 'Matrix',
schema: MatrixVizBuilderConfigSchema,
// schema: MatrixVizBuilderConfigSchema,
vizMatchesType: viz => viz.type === 'matrix',
initialConfig: {
type: 'matrix',
Expand Down
4 changes: 4 additions & 0 deletions packages/admin-panel/src/importExport/ExportModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const ExportModal = React.memo(
exportButtonText,
cancelButtonText,
isExportingMessage,
extraQueryParameters,
onCloseModal,
}) => {
const api = useApiContext();
Expand Down Expand Up @@ -60,6 +61,7 @@ export const ExportModal = React.memo(
const queryParameters = {
respondWithEmailTimeout: 10 * 1000, // if an export doesn't finish in 10 seconds, email results
...values,
...extraQueryParameters,
};
const { body: response } = await api.download(endpoint, queryParameters, fileName);
if (response?.emailTimeoutHit) {
Expand Down Expand Up @@ -157,6 +159,7 @@ ExportModal.propTypes = {
cancelButtonText: PropTypes.string,
isExportingMessage: PropTypes.string,
onCloseModal: PropTypes.func,
extraQueryParameters: PropTypes.object,
};

ExportModal.defaultProps = {
Expand All @@ -168,4 +171,5 @@ ExportModal.defaultProps = {
'Export is taking a while, and will continue in the background. You will be emailed the exported file when the process completes.',
values: {},
onCloseModal: null,
extraQueryParameters: {},
};
3 changes: 2 additions & 1 deletion packages/central-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"morgan": "^1.9.0",
"multer": "^1.4.3",
"node-schedule": "^2.1.1",
"public-ip": "^2.5.0",
"public-ip": "4.0.4",
"rate-limiter-flexible": "^5.0.3",
"react-autobind": "^1.0.6",
"react-native-uuid": "^1.4.9",
"s3urls": "^1.5.2",
Expand Down
3 changes: 1 addition & 2 deletions packages/central-server/src/apiV2/DownloadFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
getUniqueFileNameParts,
getDeDuplicatedFileName,
} from '@tupaia/utils';
import { S3, S3Client } from '@tupaia/server-utils';
import { S3, S3Client, getTempDirectory } from '@tupaia/server-utils';
import { RouteHandler } from './RouteHandler';
import { getTempDirectory } from '../utilities';
import { zipMultipleFiles } from './utilities';

export class DownloadFiles extends RouteHandler {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { RateLimiterPostgres } from 'rate-limiter-flexible';

// Limit the number of wrong attempts per day per IP to 100 for the unit tests
const MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = 100;

/**
* Singleton instances of RateLimiterPostgres
*/
let postgresRateLimiter = null;

/**
* Rate limiter which limits the number of wrong attempts per day per IP
*/
export class BruteForceRateLimiter {
constructor(database) {
if (!postgresRateLimiter) {
postgresRateLimiter = new RateLimiterPostgres({
tableCreated: true,
tableName: 'login_attempts',
storeClient: database.connection,
storeType: 'knex',
keyPrefix: 'login_fail_ip_per_day',
points: this.getMaxAttempts(),
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});
}
// Reset the points with getMaxAttempts for test mocking
postgresRateLimiter.points = this.getMaxAttempts();
this.postgresRateLimiter = postgresRateLimiter;
}

/**
* Get the maximum number of failed attempts allowed per day. Useful for testing.
* @returns {number}
*/
getMaxAttempts() {
return MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY;
}

/**
* Generate a key for the postgresRateLimiter based on the ip
* @returns {string}
*/
getIPkey(req) {
return req.ip;
}

/**
* Check if the user is rate limited
* @returns {Promise<boolean>}
*/
async checkIsRateLimited(req) {
const slowBruteForceResponder = await this.postgresRateLimiter.get(this.getIPkey(req));
return (
slowBruteForceResponder !== null &&
slowBruteForceResponder.consumedPoints >= this.getMaxAttempts()
);
}

/**
* Get the time until the user can retry.
* @returns {Promise<number>} Returns a number in milliseconds
*/
async getRetryAfter(req) {
try {
await this.postgresRateLimiter.consume(this.getIPkey(req));
} catch (rlRejected) {
return rlRejected.msBeforeNext;
}
}

/**
* Add a failed attempt to the rate limiter login_attempts table
*/
async addFailedAttempt(req) {
try {
// Add a failed attempt to the rate limiter. Gets stored in the login_attempts table
await this.postgresRateLimiter.consume(this.getIPkey(req));
} catch (rlRejected) {
// node-rate-limiter is designed to reject the promise when saving failed attempts
// We swallow the error here and let the original error bubble up
}
}

async resetFailedAttempts(req) {
await this.postgresRateLimiter.delete(this.getIPkey(req));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { RateLimiterPostgres } from 'rate-limiter-flexible';

const MAX_CONSECUTIVE_FAILS_BY_USERNAME = 10;

/**
* Singleton instances of RateLimiterPostgres
*/
let postgresRateLimiter = null;

/**
* Rate limiter which limits the number of consecutive failed attempts by username
*/
export class ConsecutiveFailsRateLimiter {
constructor(database) {
if (!postgresRateLimiter) {
postgresRateLimiter = new RateLimiterPostgres({
tableCreated: true,
tableName: 'login_attempts',
storeClient: database.connection,
storeType: 'knex',
keyPrefix: 'login_fail_consecutive_username',
points: this.getMaxAttempts(),
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 15, // Block for 15 minutes
});
}
// Reset the points with getMaxAttempts for test mocking
postgresRateLimiter.points = this.getMaxAttempts();
this.postgresRateLimiter = postgresRateLimiter;
}

/**
* Get the maximum number of consecutive failed attempts allowed. Useful for testing.
* @returns {number}
*/
getMaxAttempts() {
return MAX_CONSECUTIVE_FAILS_BY_USERNAME;
}

/**
* Generate a key for the postgresRateLimiter based on the username
* @returns {string}
*/
getUsernameKey(req) {
const { body } = req;
return body.emailAddress;
}

/**
* Check if the user is rate limited
* @returns {Promise<boolean>}
*/
async checkIsRateLimited(req) {
const maxConsecutiveFailsResponder = await this.postgresRateLimiter.get(
this.getUsernameKey(req),
);
return (
maxConsecutiveFailsResponder !== null &&
maxConsecutiveFailsResponder.consumedPoints >= this.getMaxAttempts()
);
}

/**
* Get the time until the user can retry.
* @returns {Promise<number>} Returns a number in milliseconds
*/
async getRetryAfter(req) {
try {
await this.postgresRateLimiter.consume(this.getUsernameKey(req));
} catch (rlRejected) {
return rlRejected.msBeforeNext;
}
}

async addFailedAttempt(req) {
try {
// Add a failed attempt to the rate limiter. Gets stored in the login_attempts table
await this.postgresRateLimiter.consume(this.getUsernameKey(req));
} catch (rlRejected) {
// node-rate-limiter is designed to reject the promise when saving failed attempts
// We swallow the error here and let the original error bubble up
}
}

async resetFailedAttempts(req) {
await this.postgresRateLimiter.delete(this.getUsernameKey(req));
}
}
Loading

0 comments on commit d54a9d2

Please sign in to comment.