Skip to content

Commit

Permalink
Merge branch 'dev' into rn-1108-prevent-too-many-login-attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
tcaiger committed Oct 21, 2024
2 parents f64b91e + 13ed0c9 commit 2280165
Show file tree
Hide file tree
Showing 64 changed files with 796 additions and 998 deletions.
2 changes: 1 addition & 1 deletion packages/admin-panel/src/importExport/ExportButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const ExportButton = ({ actionConfig, row }) => {
await api.download(
endpoint,
{ queryParameters, ...extraQueryParameters },
`${processedFileName}.json`,
processedFileName,
);
}}
>
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/userAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const getAuthorizationObject = async ({
email: user.email,
profileImage: user.profile_image,
verifiedEmail: user.verified_email,
preferences: user.preferences,
accessPolicy,
};
if (permissionGroups) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import { getLeaderboard } from '../../modelClasses/SurveyResponse';

const USERS_EXCLUDED_FROM_LEADER_BOARD = [
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
"'[email protected]'",
];
const SYSTEM_USERS = ["'[email protected]'", "'[email protected]'", "'[email protected]'"];

const whitespace = /\s/g;
const expectToBe = (expected, received) => {
expect(received.replace(whitespace, '')).toBe(expected.replace(whitespace, ''));
};

describe('getLeaderboard()', () => {
it('should filter out internal users on standard projects', async () => {
const expectedLeaderboard = `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
WHERE survey.project_id = ?
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${[...SYSTEM_USERS, ...USERS_EXCLUDED_FROM_LEADER_BOARD].join(', ')})
AND email NOT LIKE '%@beyondessential.com.au' AND email NOT LIKE '%@bes.au'
ORDER BY coconuts DESC
LIMIT ?;`;

expectToBe(getLeaderboard('5dfc6eaf61f76a497716cddf'), expectedLeaderboard);
});

it('should not filter out internal users on internal projects', async () => {
const INTERNAL_PROJECT_IDS = [
'6684ac9d0f018e110b000a00', // bes_asset_demo
'66a03660718c54751609eeed', // bes_asset_tracker
'6704622a45a4fc4941071605', // bes_reporting
];
const expectedLeaderboard = `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
WHERE survey.project_id = ?
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${SYSTEM_USERS.join(', ')})
ORDER BY coconuts DESC
LIMIT ?;`;

INTERNAL_PROJECT_IDS.forEach(projectId => {
expectToBe(getLeaderboard(projectId), expectedLeaderboard);
});
});
});
54 changes: 35 additions & 19 deletions packages/database/src/modelClasses/SurveyResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,44 @@ const USERS_EXCLUDED_FROM_LEADER_BOARD = [
"'[email protected]'", // Andrew
"'[email protected]'", // Gerry K
"'[email protected]'", // Geoff F
"'[email protected]'", // mSupply API Client
"'[email protected]'", // Laos Schools Data Collector
];
const SYSTEM_USERS = [
"'[email protected]'", // Tamanu Server
"'[email protected]'", // Public User
"'[email protected]'", // mSupply API Client
];
const INTERNAL_EMAIL = ['@beyondessential.com.au', '@bes.au'];
const INTERNAL_PROJECT_IDS = [
'6684ac9d0f018e110b000a00', // bes_asset_demo
'66a03660718c54751609eeed', // bes_asset_tracker
'6704622a45a4fc4941071605', // bes_reporting
];

export function getLeaderboard(projectId = '') {
const isInternalProject = projectId && INTERNAL_PROJECT_IDS.includes(projectId);

const besUsersFilter = `AND ${INTERNAL_EMAIL.map(email => `email NOT LIKE '%${email}'`).join(' AND ')}`;
const excludedUserAccountList = isInternalProject
? SYSTEM_USERS
: [...SYSTEM_USERS, ...USERS_EXCLUDED_FROM_LEADER_BOARD];

// FLOOR to force result to be returned as int, not string
return `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
${projectId ? 'WHERE survey.project_id = ?' : ''}
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${excludedUserAccountList.join(',')})
${!isInternalProject ? besUsersFilter : ''}
ORDER BY coconuts DESC
LIMIT ?;
`;
}

export class SurveyResponseRecord extends DatabaseRecord {
static databaseRecord = RECORDS.SURVEY_RESPONSE;
Expand All @@ -38,23 +70,7 @@ export class SurveyResponseModel extends MaterializedViewLogDatabaseModel {

async getLeaderboard(projectId = '', rowCount = 10) {
const bindings = projectId ? [projectId, rowCount] : [rowCount];
return this.database.executeSql(
`SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
-- ^~~~~~~~~~~~~~~ FLOOR to force result to be returned as int, not string
FROM survey_response
JOIN survey on survey.id=survey_id
${projectId ? 'WHERE survey.project_id = ?' : ''}
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE ${INTERNAL_EMAIL.map(email => `email NOT LIKE '%${email}'`).join(' AND ')}
AND email NOT IN (${USERS_EXCLUDED_FROM_LEADER_BOARD.join(',')})
ORDER BY coconuts DESC
LIMIT ?;
`,
bindings,
);
const query = getLeaderboard(projectId);
return this.database.executeSql(query, bindings);
}
}
26 changes: 24 additions & 2 deletions packages/datatrak-web/src/api/queries/useProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ import { useQuery } from '@tanstack/react-query';
import { DatatrakWebProjectsRequest } from '@tupaia/types';
import { get } from '../api';

export const useProjects = () => {
return useQuery(['projects'], (): Promise<DatatrakWebProjectsRequest.ResBody> => get('projects'));
export const useProjects = (sortByAccess = true) => {
const { data, ...query } = useQuery(
['projects'],
(): Promise<DatatrakWebProjectsRequest.ResBody> => get('projects'),
);

if (data && sortByAccess) {
data.sort((a, b) => {
// Sort by hasAccess = true first
if (a.hasAccess !== b.hasAccess) {
return a.hasAccess ? -1 : 1;
}

// Sort by hasPendingAccess = true second
if (a.hasPendingAccess !== b.hasPendingAccess) {
return a.hasPendingAccess ? -1 : 1;
}

// Otherwise, sort alphabetically by name
return a.name.localeCompare(b.name);
});
}

return { ...query, data };
};
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ChangeProjectButton = ({ className }: { className?: string }) => {
<ProjectButton onClick={openProjectModal} tooltip="Change project">
{projectName ?? 'Select project'}
</ProjectButton>
{projectModalIsOpen && <ProjectSelectModal onClose={closeProjectModal} />}
{projectModalIsOpen && <ProjectSelectModal onBack={closeProjectModal} />}
</Container>
);
};
7 changes: 0 additions & 7 deletions packages/datatrak-web/src/components/SelectList/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/datatrak-web/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

export { PageContainer } from './PageContainer';
export * from './Icons';
export * from './SelectList';
export { Autocomplete, QuestionAutocomplete } from './Autocomplete';
export { Button } from './Button';
export { ButtonLink } from './ButtonLink';
Expand Down
20 changes: 17 additions & 3 deletions packages/datatrak-web/src/features/EntitySelector/ResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
import { FormLabelProps, Typography } from '@material-ui/core';
import RoomIcon from '@material-ui/icons/Room';
import { DatatrakWebEntityDescendantsRequest } from '@tupaia/types';
import { ListItemType, SelectList } from '../../components';
import { SelectList } from '@tupaia/ui-components';

const DARK_BLUE = '#004975';

Expand Down Expand Up @@ -43,6 +43,20 @@ export const ResultItem = ({ name, parentName }) => {
);
};

type ListItemType = Record<string, unknown> & {
children?: ListItemType[];
content: string | ReactNode;
value: string;
selected?: boolean;
icon?: ReactNode;
tooltip?: string;
button?: boolean;
disabled?: boolean;
labelProps?: FormLabelProps & {
component?: React.ElementType;
};
};

type SearchResults = DatatrakWebEntityDescendantsRequest.ResBody;
interface ResultsListProps {
value: string;
Expand Down
19 changes: 17 additions & 2 deletions packages/datatrak-web/src/features/GroupedSurveyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React, { useEffect } from 'react';
import React, { ReactNode, useEffect } from 'react';
import styled from 'styled-components';
import { FormHelperText, FormLabelProps } from '@material-ui/core';
import { Country } from '@tupaia/types';
import { ListItemType, SelectList, SurveyFolderIcon, SurveyIcon } from '../components';
import { SelectList } from '@tupaia/ui-components';
import { SurveyFolderIcon, SurveyIcon } from '../components';
import { Survey } from '../types';
import { useCurrentUserContext, useProjectSurveys } from '../api';

Expand All @@ -21,6 +22,20 @@ const ListWrapper = styled.div`
}
`;

type ListItemType = Record<string, unknown> & {
children?: ListItemType[];
content: string | ReactNode;
value: string;
selected?: boolean;
icon?: ReactNode;
tooltip?: string;
button?: boolean;
disabled?: boolean;
labelProps?: FormLabelProps & {
component?: React.ElementType;
};
};

const sortAlphanumerically = (a: ListItemType, b: ListItemType) => {
return (a.content as string).trim()?.localeCompare((b.content as string).trim(), 'en', {
numeric: true,
Expand Down
6 changes: 3 additions & 3 deletions packages/datatrak-web/src/features/RequestProjectAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import { useCountryAccessList, useProject, useRequestProjectAccess } from '../ap
interface RequestProjectAccessProps {
variant?: 'page' | 'modal';
projectCode?: string;
onClose?: () => void;
onBack?: () => void;
}

export const RequestProjectAccess = ({ projectCode, onClose }: RequestProjectAccessProps) => {
export const RequestProjectAccess = ({ projectCode, onBack }: RequestProjectAccessProps) => {
const { data: project, isLoading: isLoadingProject, isFetched } = useProject(projectCode);
const { mutate: requestProjectAccess, isLoading, isSuccess } = useRequestProjectAccess();
const { data: countries } = useCountryAccessList(projectCode);

return (
<UIRequestProjectAccess
onClose={onClose}
onBack={onBack}
project={project}
onSubmit={requestProjectAccess}
isLoading={isLoadingProject}
Expand Down
1 change: 0 additions & 1 deletion packages/datatrak-web/src/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

export { ProjectSelectForm } from './ProjectSelectForm';
export {
SurveyScreen,
SurveySuccessScreen,
Expand Down
21 changes: 14 additions & 7 deletions packages/datatrak-web/src/layout/UserMenu/ProjectSelectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { Paper } from '@material-ui/core';
import { ProjectSelectForm, RequestProjectAccess } from '../../features';
import { useCurrentUserContext } from '../../api';
import { ProjectSelectForm } from '@tupaia/ui-components';
import { RequestProjectAccess } from '../../features';
import { useCurrentUserContext, useEditUser, useProjects } from '../../api';
import { Modal } from '../../components';

const Wrapper = styled(Paper)`
Expand All @@ -20,27 +21,33 @@ const Wrapper = styled(Paper)`
`;

interface ModalProps {
onClose: () => void;
onBack: () => void;
}

export const ProjectSelectModal = ({ onClose }: ModalProps) => {
export const ProjectSelectModal = ({ onBack }: ModalProps) => {
const { projectId } = useCurrentUserContext();
const [requestAccessProjectCode, setRequestAccessProjectCode] = useState<string | null>(null);
const { data: projects, isLoading } = useProjects();
const { mutate: onConfirm, isLoading: isConfirming } = useEditUser(onBack);

return (
// Enable the portal so it displays over any other content and we don't get z-index issues
<Modal open onClose={onClose} PaperComponent={Wrapper} disablePortal={false}>
<Modal open onClose={onBack} PaperComponent={Wrapper} disablePortal={false}>
{requestAccessProjectCode ? (
<RequestProjectAccess
projectCode={requestAccessProjectCode}
onClose={() => setRequestAccessProjectCode(null)}
onBack={() => setRequestAccessProjectCode(null)}
/>
) : (
<ProjectSelectForm
variant="modal"
projectId={projectId}
onClose={onClose}
onClose={onBack}
onRequestAccess={setRequestAccessProjectCode}
projects={projects}
isLoading={isLoading}
onConfirm={onConfirm}
isConfirming={isConfirming}
/>
)}
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion packages/datatrak-web/src/layout/UserMenu/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const UserMenu = () => {
onCloseMenu={onCloseMenu}
openProjectModal={openProjectModal}
/>
{projectModalOpen && <ProjectSelectModal onClose={closeProjectModal} />}
{projectModalOpen && <ProjectSelectModal onBack={closeProjectModal} />}
</Wrapper>
);
};
Loading

0 comments on commit 2280165

Please sign in to comment.