From 7809a0575d080f0538f0633ec3fd09eb09323028 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Mon, 15 Nov 2021 22:37:25 -0800 Subject: [PATCH 01/18] initial commit --- api/src/models/user.ts | 2 + api/src/paths/user.ts | 5 +- api/src/paths/user/{userId}/delete.ts | 128 +++++++++++++ api/src/paths/users.ts | 19 +- api/src/queries/users/system-role-queries.ts | 33 ++++ api/src/queries/users/user-queries.ts | 9 +- app/src/components/dialog/YesNoDialog.tsx | 28 ++- app/src/components/toolbar/ActionToolbars.tsx | 171 ++++++++++++++++++ .../features/admin/users/ActiveUsersList.tsx | 162 +++++++++++------ .../features/admin/users/ManageUsersPage.tsx | 2 +- app/src/hooks/api/useUserApi.ts | 14 +- 11 files changed, 514 insertions(+), 59 deletions(-) create mode 100644 api/src/paths/user/{userId}/delete.ts create mode 100644 app/src/components/toolbar/ActionToolbars.tsx diff --git a/api/src/models/user.ts b/api/src/models/user.ts index 5897f4ebe8..e28b7ea413 100644 --- a/api/src/models/user.ts +++ b/api/src/models/user.ts @@ -5,6 +5,7 @@ const defaultLog = getLogger('models/user'); export class UserObject { id: number; user_identifier: string; + user_record_end_date: string; role_ids: number[]; role_names: string[]; @@ -13,6 +14,7 @@ export class UserObject { this.id = obj?.id || null; this.user_identifier = obj?.user_identifier || null; + this.user_record_end_date = obj?.user_record_end_date || null; this.role_ids = (obj?.role_ids?.length && obj.role_ids) || []; this.role_names = (obj?.role_names?.length && obj.role_names) || []; } diff --git a/api/src/paths/user.ts b/api/src/paths/user.ts index 81d691fc99..58adc3e591 100644 --- a/api/src/paths/user.ts +++ b/api/src/paths/user.ts @@ -9,7 +9,7 @@ import { getLogger } from '../utils/logger'; const defaultLog = getLogger('paths/user'); -export const POST: Operation = [ +export const DELETE: Operation = [ authorizeRequestHandler(() => { return { and: [ @@ -23,7 +23,7 @@ export const POST: Operation = [ addUser() ]; -POST.apiDoc = { +DELETE.apiDoc = { description: 'Add a new system user.', tags: ['user'], security: [ @@ -122,6 +122,7 @@ export function addUser(): RequestHandler { return res.send(200); } catch (error) { defaultLog.error({ label: 'getUser', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/user/{userId}/delete.ts b/api/src/paths/user/{userId}/delete.ts new file mode 100644 index 0000000000..fbf7522e1e --- /dev/null +++ b/api/src/paths/user/{userId}/delete.ts @@ -0,0 +1,128 @@ +'use strict'; + +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../constants/roles'; +import { getDBConnection } from '../../../database/db'; +import { HTTP400, HTTP500 } from '../../../errors/CustomError'; +import { deleteSystemUserSQL } from '../../../queries/users/system-role-queries'; +import { getUserByIdSQL } from '../../../queries/users/user-queries'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/delete'); + +export const DELETE: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + removeSystemUser() +]; + +DELETE.apiDoc = { + description: 'Remove a user from the system.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Remove system user from system OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function removeSystemUser(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'removeSystemUser', message: 'params', req_params: req.params }); + + const userId = (req.params && Number(req.params.userId)) || null; + + if (!userId) { + throw new HTTP400('Missing required path param: userId'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + // Get the system user + const getUserSQLStatement = getUserByIdSQL(userId); + + if (!getUserSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + await connection.open(); + + const getUserResponse = await connection.query(getUserSQLStatement.text, getUserSQLStatement.values); + + const userResult = (getUserResponse && getUserResponse.rowCount && getUserResponse.rows[0]) || null; + + if (!userResult) { + throw new HTTP400('Failed to get system user'); + } + + if (userResult.user_record_end_date) { + throw new HTTP400('The system user is not active'); + } + + const deleteSystemUserSqlStatement = deleteSystemUserSQL(userId); + + if (!deleteSystemUserSqlStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const response = await connection.query(deleteSystemUserSqlStatement.text, deleteSystemUserSqlStatement.values); + + await connection.commit(); + + const result = (response && response.rowCount) || null; + + if (!result) { + throw new HTTP500('Failed to remove user from the system'); + } + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'removeSystemUser', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/users.ts b/api/src/paths/users.ts index b7ffe50fc6..c38275aec5 100644 --- a/api/src/paths/users.ts +++ b/api/src/paths/users.ts @@ -42,7 +42,24 @@ GET.apiDoc = { title: 'User Response Object', type: 'object', properties: { - // TODO needs finalizing (here and in the user-queries.ts SQL) + id: { + type: 'number' + }, + user_identifier: { + type: 'string' + }, + role_ids: { + type: 'array', + items: { + type: 'string' + } + }, + role_names: { + type: 'array', + items: { + type: 'string' + } + } } } } diff --git a/api/src/queries/users/system-role-queries.ts b/api/src/queries/users/system-role-queries.ts index 9a080fe77a..4bd092b31e 100644 --- a/api/src/queries/users/system-role-queries.ts +++ b/api/src/queries/users/system-role-queries.ts @@ -87,6 +87,39 @@ export const deleteSystemRolesSQL = (userId: number, roleIds: number[]): SQLStat return sqlStatement; }; +/** + * SQL query to remove one or more system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deleteSystemUserSQL = (userId: number): SQLStatement | null => { + defaultLog.debug({ label: 'deleteSystemRolesSQL', message: 'params' }); + + if (!userId) { + return null; + } + + const sqlStatement = SQL` + UPDATE + system_user + SET + record_end_date = now() + WHERE + system_user_id = ${userId}; + `; + + defaultLog.debug({ + label: 'deleteSystemUserSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; + /** * SQL query to add a single project role to a user. * diff --git a/api/src/queries/users/user-queries.ts b/api/src/queries/users/user-queries.ts index 932c939592..bd9172c30b 100644 --- a/api/src/queries/users/user-queries.ts +++ b/api/src/queries/users/user-queries.ts @@ -66,6 +66,7 @@ export const getUserByIdSQL = (userId: number): SQLStatement | null => { SELECT su.system_user_id as id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -82,6 +83,7 @@ export const getUserByIdSQL = (userId: number): SQLStatement | null => { su.system_user_id = ${userId} GROUP BY su.system_user_id, + su.record_end_date, su.user_identifier; `; @@ -107,6 +109,7 @@ export const getUserListSQL = (): SQLStatement | null => { SELECT su.system_user_id as id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -119,9 +122,13 @@ export const getUserListSQL = (): SQLStatement | null => { system_role sr ON sur.system_role_id = sr.system_role_id + WHERE + su.record_end_date IS NULL GROUP BY su.system_user_id, - su.user_identifier; + su.user_identifier, + su.record_end_date + ; `; defaultLog.debug({ diff --git a/app/src/components/dialog/YesNoDialog.tsx b/app/src/components/dialog/YesNoDialog.tsx index 26e1bda78d..dc4e649ad1 100644 --- a/app/src/components/dialog/YesNoDialog.tsx +++ b/app/src/components/dialog/YesNoDialog.tsx @@ -46,6 +46,30 @@ export interface IYesNoDialogProps { * @memberof IYesNoDialogProps */ onYes: () => void; + + /** + * The yes button label. + * + * @type {string} + * @memberof IYesNoDialogProps + */ + yesButtonLabel?: string; + + /** + * The no button label. + * + * @type {string} + * @memberof IYesNoDialogProps + */ + noButtonLabel?: string; + + /** + * The no button label. + * + * @type {string} + * @memberof IYesNoDialogProps + */ + yesButtonColor?: string; } /** @@ -73,10 +97,10 @@ const YesNoDialog: React.FC = (props) => { diff --git a/app/src/components/toolbar/ActionToolbars.tsx b/app/src/components/toolbar/ActionToolbars.tsx new file mode 100644 index 0000000000..4932be0b47 --- /dev/null +++ b/app/src/components/toolbar/ActionToolbars.tsx @@ -0,0 +1,171 @@ +import Box from '@material-ui/core/Box'; +import Button, { ButtonProps } from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; +import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import React, { ReactNode, useState } from 'react'; + +export interface ICustomButtonProps { + buttonLabel: string; + buttonTitle: string; + buttonOnClick: () => void; + buttonStartIcon: ReactNode; + buttonEndIcon?: ReactNode; + buttonProps?: Partial; +} + +export interface IButtonToolbarProps extends ICustomButtonProps, IActionToolbarProps {} + +export const H3ButtonToolbar: React.FC = (props) => { + const id = `h3-button-toolbar-${props.buttonLabel.replace(/\s/g, '')}`; + + return ( + + + + ); +}; + +export const H2ButtonToolbar: React.FC = (props) => { + const id = `h2-button-toolbar-${props.buttonLabel.replace(/\s/g, '')}`; + + return ( + + + + ); +}; + +export interface IMenuToolbarItem { + menuIcon?: ReactNode; + menuLabel: string; + menuOnClick: () => void; +} + +export interface IMenuToolbarProps extends ICustomMenuButtonProps, IActionToolbarProps {} + +export const H2MenuToolbar: React.FC = (props) => { + return ( + + + + ); +}; + +export interface ICustomMenuButtonProps { + buttonLabel?: string; + buttonTitle?: string; + buttonStartIcon?: ReactNode; + buttonEndIcon?: ReactNode; + buttonProps?: Partial; + menuItems: IMenuToolbarItem[]; +} + +export const CustomMenuButton: React.FC = (props) => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const id = `h2-menu-toolbar-${props.buttonLabel?.replace(/\s/g, '') || 'button'}`; + + const handleClick = (event: any) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const closeMenuOnItemClick = (menuItemOnClick: () => void) => { + setAnchorEl(null); + menuItemOnClick(); + }; + + return ( + <> + + + {props.menuItems.map((menuItem) => { + const id = `h2-menu-toolbar-item-${menuItem.menuLabel.replace(/\s/g, '')}`; + return ( + closeMenuOnItemClick(menuItem.menuOnClick)}> + {menuItem.menuIcon} + {menuItem.menuLabel} + + ); + })} + + + ); +}; + +interface IActionToolbarProps { + label: string; + labelProps?: Partial>; + toolbarProps?: Partial; +} + +const ActionToolbar: React.FC = (props) => { + return ( + + + {props.label} + + {props.children} + + ); +}; diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/ActiveUsersList.tsx index 920307d871..75597f189e 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/ActiveUsersList.tsx @@ -8,12 +8,18 @@ import TableHead from '@material-ui/core/TableHead'; import TablePagination from '@material-ui/core/TablePagination'; import TableRow from '@material-ui/core/TableRow'; import Typography from '@material-ui/core/Typography'; +import { CustomMenuButton } from 'components/toolbar/ActionToolbars'; import { IGetUserResponse } from 'interfaces/useUserApi.interface'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { handleChangeRowsPerPage, handleChangePage } from 'utils/tablePaginationUtils'; +import { mdiDotsVertical, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { DialogContext } from 'contexts/dialogContext'; export interface IActiveUsersListProps { activeUsers: IGetUserResponse[]; + getUsers: (forceFetch: boolean) => void; } /** @@ -23,64 +29,118 @@ export interface IActiveUsersListProps { * @return {*} */ const ActiveUsersList: React.FC = (props) => { + const biohubApi = useBiohubApi(); const { activeUsers } = props; const [rowsPerPage, setRowsPerPage] = useState(5); const [page, setPage] = useState(0); + const dialogContext = useContext(DialogContext); + + const handleRemoveUserClick = (row: any) => { + dialogContext.setYesNoDialog({ + dialogTitle: 'Remove user?', + dialogText: + 'Removing user will remove their access to this application and all related projects. Are you sure you want to proceed?', + yesButtonLabel: 'Remove User', + noButtonLabel: 'Cancel', + onClose: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onNo: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + open: true, + onYes: () => { + deleteSystemUser(row); + dialogContext.setYesNoDialog({ open: false }); + } + }); + }; + + const deleteSystemUser = async (user: any) => { + if (!user?.id) { + return; + } + try { + const response = await biohubApi.user.removeSystemUser(user.id); + + console.log(response); + + props.getUsers(true); + } catch (error) { + return error; + } + }; return ( - - - Active Users ({activeUsers?.length || 0}) - - - - - - Name - Username - Company - Regional Offices - Roles - Last Active - - - - {!activeUsers?.length && ( - - - No Active Users - + <> + + + Active Users ({activeUsers?.length || 0}) + + +
+ + + Name + Username + Company + Regional Offices + Roles + Last Active + Actions - )} - {activeUsers.length > 0 && - activeUsers.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => ( - - - {row.user_identifier || 'Not Applicable'} - - - {row.role_names.join(', ') || 'Not Applicable'} - + + + {!activeUsers?.length && ( + + + No Active Users + - ))} - -
-
- {activeUsers?.length > 0 && ( - handleChangePage(event, newPage, setPage)} - onChangeRowsPerPage={(event: React.ChangeEvent) => - handleChangeRowsPerPage(event, setPage, setRowsPerPage) - } - /> - )} -
+ )} + {activeUsers.length > 0 && + activeUsers.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => ( + + + {row.user_identifier || 'Not Applicable'} + + + {row.role_names.join(', ') || 'Not Applicable'} + + + } + menuItems={[ + { + menuIcon: , + menuLabel: 'Remove User', + menuOnClick: () => handleRemoveUserClick(row) + } + ]} + /> + + + ))} + + + + {activeUsers?.length > 0 && ( + handleChangePage(event, newPage, setPage)} + onChangeRowsPerPage={(event: React.ChangeEvent) => + handleChangeRowsPerPage(event, setPage, setRowsPerPage) + } + /> + )} + + ); }; diff --git a/app/src/features/admin/users/ManageUsersPage.tsx b/app/src/features/admin/users/ManageUsersPage.tsx index 8221d5e3ea..d9a3be9215 100644 --- a/app/src/features/admin/users/ManageUsersPage.tsx +++ b/app/src/features/admin/users/ManageUsersPage.tsx @@ -133,7 +133,7 @@ const ManageUsersPage: React.FC = () => { /> - + diff --git a/app/src/hooks/api/useUserApi.ts b/app/src/hooks/api/useUserApi.ts index a9c8e9fc81..b7bd8e5cca 100644 --- a/app/src/hooks/api/useUserApi.ts +++ b/app/src/hooks/api/useUserApi.ts @@ -30,9 +30,21 @@ const useUserApi = (axios: AxiosInstance) => { return data; }; + /** + * Get user details for all users. + * + * @return {*} {Promise} + */ + const removeSystemUser = async (userId: number): Promise => { + const { data } = await axios.delete(`/api/user/${userId}/delete`); + + return data; + }; + return { getUser, - getUsersList + getUsersList, + removeSystemUser }; }; From 5e0e10f3bd22e6dc0a4a7b49410b36f58491c368 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Tue, 16 Nov 2021 10:38:52 -0800 Subject: [PATCH 02/18] query change --- .../project-participation-queries.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/api/src/queries/project-participation/project-participation-queries.ts b/api/src/queries/project-participation/project-participation-queries.ts index 5c7f735378..aff8aaf06d 100644 --- a/api/src/queries/project-participation/project-participation-queries.ts +++ b/api/src/queries/project-participation/project-participation-queries.ts @@ -27,24 +27,32 @@ export const getProjectParticipationBySystemUserSQL = ( } const sqlStatement = SQL` - SELECT - pp.project_id, - pp.system_user_id, - array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, - array_remove(array_agg(pr.name), NULL) AS project_role_names - FROM - project_participation pp - LEFT JOIN - project_role pr - ON - pp.project_role_id = pr.project_role_id - WHERE - pp.project_id = ${projectId} - AND - pp.system_user_id = ${systemUserId} - GROUP BY - pp.project_id, - pp.system_user_id; + SELECT + pp.project_id, + pp.system_user_id, + su.record_end_date, + array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, + array_remove(array_agg(pr.name), NULL) AS project_role_names + FROM + project_participation pp + LEFT JOIN + project_role pr + ON + pp.project_role_id = pr.project_role_id + LEFT JOIN + system_user su + ON + pp.system_user_id = su.system_user_id + WHERE + pp.project_id = ${projectId} + AND + pp.system_user_id = ${systemUserId} + AND + su.record_end_date is NULL + GROUP BY + pp.project_id, + pp.system_user_id, + su.record_end_date ; `; defaultLog.info({ From 0c6a43f52534aafe1b68d538c5cf73687809d7fa Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Tue, 16 Nov 2021 10:47:35 -0800 Subject: [PATCH 03/18] more work on user access --- api/src/paths/user/{userId}/delete.ts | 4 +-- api/src/queries/users/system-role-queries.ts | 32 ----------------- api/src/queries/users/user-queries.ts | 34 +++++++++++++++++++ .../security/authorization.ts | 15 +++++--- .../features/admin/users/ActiveUsersList.tsx | 11 +++--- app/src/hooks/api/useUserApi.ts | 4 +-- app/src/hooks/useKeycloakWrapper.tsx | 2 +- app/src/interfaces/useUserApi.interface.ts | 1 + 8 files changed, 55 insertions(+), 48 deletions(-) diff --git a/api/src/paths/user/{userId}/delete.ts b/api/src/paths/user/{userId}/delete.ts index fbf7522e1e..70dac751c8 100644 --- a/api/src/paths/user/{userId}/delete.ts +++ b/api/src/paths/user/{userId}/delete.ts @@ -5,7 +5,7 @@ import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; import { HTTP400, HTTP500 } from '../../../errors/CustomError'; -import { deleteSystemUserSQL } from '../../../queries/users/system-role-queries'; +import { deActivateSystemUserSQL } from '../../../queries/users/user-queries'; import { getUserByIdSQL } from '../../../queries/users/user-queries'; import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; import { getLogger } from '../../../utils/logger'; @@ -100,7 +100,7 @@ export function removeSystemUser(): RequestHandler { throw new HTTP400('The system user is not active'); } - const deleteSystemUserSqlStatement = deleteSystemUserSQL(userId); + const deleteSystemUserSqlStatement = deActivateSystemUserSQL(userId); if (!deleteSystemUserSqlStatement) { throw new HTTP400('Failed to build SQL delete statement'); diff --git a/api/src/queries/users/system-role-queries.ts b/api/src/queries/users/system-role-queries.ts index 4bd092b31e..03761564ed 100644 --- a/api/src/queries/users/system-role-queries.ts +++ b/api/src/queries/users/system-role-queries.ts @@ -87,38 +87,6 @@ export const deleteSystemRolesSQL = (userId: number, roleIds: number[]): SQLStat return sqlStatement; }; -/** - * SQL query to remove one or more system roles from a user. - * - * @param {number} userId - * @param {number[]} roleIds - * @return {*} {(SQLStatement | null)} - */ -export const deleteSystemUserSQL = (userId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteSystemRolesSQL', message: 'params' }); - - if (!userId) { - return null; - } - - const sqlStatement = SQL` - UPDATE - system_user - SET - record_end_date = now() - WHERE - system_user_id = ${userId}; - `; - - defaultLog.debug({ - label: 'deleteSystemUserSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; /** * SQL query to add a single project role to a user. diff --git a/api/src/queries/users/user-queries.ts b/api/src/queries/users/user-queries.ts index bd9172c30b..1aa87bb763 100644 --- a/api/src/queries/users/user-queries.ts +++ b/api/src/queries/users/user-queries.ts @@ -194,3 +194,37 @@ export const addSystemUserSQL = ( return sqlStatement; }; + + +/** + * SQL query to remove one or more system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ + export const deActivateSystemUserSQL = (userId: number): SQLStatement | null => { + defaultLog.debug({ label: 'deActivateSystemUserSQL', message: 'params' }); + + if (!userId) { + return null; + } + + const sqlStatement = SQL` + UPDATE + system_user + SET + record_end_date = now() + WHERE + system_user_id = ${userId}; + `; + + defaultLog.debug({ + label: 'deleteSystemUserSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/api/src/request-handlers/security/authorization.ts b/api/src/request-handlers/security/authorization.ts index cecc7e7871..41471b3fa6 100644 --- a/api/src/request-handlers/security/authorization.ts +++ b/api/src/request-handlers/security/authorization.ts @@ -210,8 +210,13 @@ export const authorizeBySystemRole = async ( return false; } + if (systemUserObject.user_record_end_date){ + //system user has an expired record + return false; + } + // Check if the user has at least 1 of the valid roles - return userHasValidRole(authorizeSystemRoles.validSystemRoles, systemUserObject?.role_names); + return userHasValidRole(authorizeSystemRoles.validSystemRoles, systemUserObject?.role_names, systemUserObject?.user_record_end_date); }; /** @@ -282,19 +287,21 @@ export const authorizeBySystemUser = async (req: Request, connection: IDBConnect * @return {*} {boolean} true if the user has at least 1 of the valid roles or no valid roles are specified, false * otherwise */ -export const userHasValidRole = function (validRoles: string | string[], userRoles: string | string[]): boolean { +export const userHasValidRole = function (validRoles: string | string[], userRoles: string | string[], userExpiryDate: string): boolean { if (!validRoles || !validRoles.length) { return true; } - if (!Array.isArray(validRoles)) { + if (!Array.isArray(validRoles) && !userExpiryDate) { validRoles = [validRoles]; } - if (!Array.isArray(userRoles)) { + if (!Array.isArray(userRoles) && !userExpiryDate) { userRoles = [userRoles]; } + + for (const validRole of validRoles) { if (userRoles.includes(validRole)) { return true; diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/ActiveUsersList.tsx index 75597f189e..ee13890ec3 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/ActiveUsersList.tsx @@ -39,8 +39,7 @@ const ActiveUsersList: React.FC = (props) => { const handleRemoveUserClick = (row: any) => { dialogContext.setYesNoDialog({ dialogTitle: 'Remove user?', - dialogText: - 'Removing user will remove their access to this application and all related projects. Are you sure you want to proceed?', + dialogText: `Removing ${row.user_identifier} will remove their access to this application and all related projects. Are you sure you want to proceed?`, yesButtonLabel: 'Remove User', noButtonLabel: 'Cancel', onClose: () => { @@ -51,20 +50,18 @@ const ActiveUsersList: React.FC = (props) => { }, open: true, onYes: () => { - deleteSystemUser(row); + deActivateSystemUser(row); dialogContext.setYesNoDialog({ open: false }); } }); }; - const deleteSystemUser = async (user: any) => { + const deActivateSystemUser = async (user: any) => { if (!user?.id) { return; } try { - const response = await biohubApi.user.removeSystemUser(user.id); - - console.log(response); + await biohubApi.user.deleteSystemUser(user.id); props.getUsers(true); } catch (error) { diff --git a/app/src/hooks/api/useUserApi.ts b/app/src/hooks/api/useUserApi.ts index b7bd8e5cca..d7c1bf5990 100644 --- a/app/src/hooks/api/useUserApi.ts +++ b/app/src/hooks/api/useUserApi.ts @@ -35,7 +35,7 @@ const useUserApi = (axios: AxiosInstance) => { * * @return {*} {Promise} */ - const removeSystemUser = async (userId: number): Promise => { + const deleteSystemUser = async (userId: number): Promise => { const { data } = await axios.delete(`/api/user/${userId}/delete`); return data; @@ -44,7 +44,7 @@ const useUserApi = (axios: AxiosInstance) => { return { getUser, getUsersList, - removeSystemUser + deleteSystemUser }; }; diff --git a/app/src/hooks/useKeycloakWrapper.tsx b/app/src/hooks/useKeycloakWrapper.tsx index c58ff2fc51..076e233632 100644 --- a/app/src/hooks/useKeycloakWrapper.tsx +++ b/app/src/hooks/useKeycloakWrapper.tsx @@ -148,7 +148,7 @@ function useKeycloakWrapper(): IKeycloakWrapper { } catch {} setBioHubUser(() => { - if (userDetails?.role_names?.length) { + if (userDetails?.role_names?.length && !userDetails?.user_record_end_date) { setHasLoadedAllUserInfo(true); } else { setShouldLoadAccessRequest(true); diff --git a/app/src/interfaces/useUserApi.interface.ts b/app/src/interfaces/useUserApi.interface.ts index f99504cde4..f6cd89714f 100644 --- a/app/src/interfaces/useUserApi.interface.ts +++ b/app/src/interfaces/useUserApi.interface.ts @@ -1,5 +1,6 @@ export interface IGetUserResponse { id: number; + user_record_end_date: string; user_identifier: string; role_names: string[]; } From 3d652c2b622da07251d3319447527266d8f32d0b Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Tue, 16 Nov 2021 15:36:14 -0800 Subject: [PATCH 04/18] removing user's roles in system and projects --- api/src/paths/access-request.ts | 24 +++- api/src/paths/user/{userId}/delete.ts | 38 +++++- api/src/queries/users/system-role-queries.ts | 1 - api/src/queries/users/user-queries.ts | 109 +++++++++++++++++- .../security/authorization.ts | 12 +- 5 files changed, 170 insertions(+), 14 deletions(-) diff --git a/api/src/paths/access-request.ts b/api/src/paths/access-request.ts index ddb13ed608..d507abfc95 100644 --- a/api/src/paths/access-request.ts +++ b/api/src/paths/access-request.ts @@ -3,10 +3,10 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { PROJECT_ROLE } from '../constants/roles'; -import { getDBConnection } from '../database/db'; +import { getDBConnection, IDBConnection } from '../database/db'; import { HTTP400, HTTP500 } from '../errors/CustomError'; import { UserObject } from '../models/user'; -import { getUserByUserIdentifierSQL } from '../queries/users/user-queries'; +import { getUserByUserIdentifierSQL, activateSystemUserSQL } from '../queries/users/user-queries'; import { authorizeRequestHandler } from '../request-handlers/security/authorization'; import { getLogger } from '../utils/logger'; import { updateAdministrativeActivity } from './administrative-activity'; @@ -166,6 +166,10 @@ export function updateAccessRequest(): RequestHandler { throw new HTTP500('Failed to get or add system user'); } + if (userData.record_end_date) { + await activateDeactivatedSystemUser(userObject.id, connection); + } + // Filter out any system roles that have already been added to the user const rolesIdsToAdd = roleIds.filter((roleId) => !userObject.role_ids.includes(roleId)); @@ -188,3 +192,19 @@ export function updateAccessRequest(): RequestHandler { } }; } + +export const activateDeactivatedSystemUser = async (systemUserId: number, connection: IDBConnection): Promise => { + const sqlStatement = activateSystemUserSQL(systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP400('Failed to activate system user'); + } + + return response?.rows?.[0] || null; +}; diff --git a/api/src/paths/user/{userId}/delete.ts b/api/src/paths/user/{userId}/delete.ts index 70dac751c8..c704a8c762 100644 --- a/api/src/paths/user/{userId}/delete.ts +++ b/api/src/paths/user/{userId}/delete.ts @@ -5,7 +5,11 @@ import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; import { HTTP400, HTTP500 } from '../../../errors/CustomError'; -import { deActivateSystemUserSQL } from '../../../queries/users/user-queries'; +import { + deActivateSystemUserSQL, + deleteAllSystemRolesSQL, + deleteAllProjectRolesSQL +} from '../../../queries/users/user-queries'; import { getUserByIdSQL } from '../../../queries/users/user-queries'; import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; import { getLogger } from '../../../utils/logger'; @@ -100,10 +104,40 @@ export function removeSystemUser(): RequestHandler { throw new HTTP400('The system user is not active'); } + const deleteAllProjectRolesSqlStatement = deleteAllProjectRolesSQL(userId); + + if (!deleteAllProjectRolesSqlStatement) { + throw new HTTP400('Failed to build SQL delete statement for deleting system roles'); + } + + const deleteProjectRolesResponse = await connection.query( + deleteAllProjectRolesSqlStatement.text, + deleteAllProjectRolesSqlStatement.values + ); + + if (!deleteProjectRolesResponse) { + throw new HTTP400('Failed to the project project roles'); + } + + const deleteSystemRoleSqlStatement = deleteAllSystemRolesSQL(userId); + + if (!deleteSystemRoleSqlStatement) { + throw new HTTP400('Failed to build SQL delete statement for deleting system roles'); + } + + const deleteSystemRolesResponse = await connection.query( + deleteSystemRoleSqlStatement.text, + deleteSystemRoleSqlStatement.values + ); + + if (!deleteSystemRolesResponse) { + throw new HTTP400('Failed to delete the user system roles'); + } + const deleteSystemUserSqlStatement = deActivateSystemUserSQL(userId); if (!deleteSystemUserSqlStatement) { - throw new HTTP400('Failed to build SQL delete statement'); + throw new HTTP400('Failed to build SQL delete statement to deactivate system user'); } const response = await connection.query(deleteSystemUserSqlStatement.text, deleteSystemUserSqlStatement.values); diff --git a/api/src/queries/users/system-role-queries.ts b/api/src/queries/users/system-role-queries.ts index 03761564ed..9a080fe77a 100644 --- a/api/src/queries/users/system-role-queries.ts +++ b/api/src/queries/users/system-role-queries.ts @@ -87,7 +87,6 @@ export const deleteSystemRolesSQL = (userId: number, roleIds: number[]): SQLStat return sqlStatement; }; - /** * SQL query to add a single project role to a user. * diff --git a/api/src/queries/users/user-queries.ts b/api/src/queries/users/user-queries.ts index 1aa87bb763..7a0846d7e9 100644 --- a/api/src/queries/users/user-queries.ts +++ b/api/src/queries/users/user-queries.ts @@ -20,6 +20,7 @@ export const getUserByUserIdentifierSQL = (userIdentifier: string): SQLStatement SELECT su.system_user_id as id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -36,6 +37,7 @@ export const getUserByUserIdentifierSQL = (userIdentifier: string): SQLStatement su.user_identifier = ${userIdentifier} GROUP BY su.system_user_id, + su.record_end_date, su.user_identifier; `; @@ -81,6 +83,8 @@ export const getUserByIdSQL = (userId: number): SQLStatement | null => { sur.system_role_id = sr.system_role_id WHERE su.system_user_id = ${userId} + AND + su.record_end_date IS NULL GROUP BY su.system_user_id, su.record_end_date, @@ -195,7 +199,6 @@ export const addSystemUserSQL = ( return sqlStatement; }; - /** * SQL query to remove one or more system roles from a user. * @@ -203,7 +206,7 @@ export const addSystemUserSQL = ( * @param {number[]} roleIds * @return {*} {(SQLStatement | null)} */ - export const deActivateSystemUserSQL = (userId: number): SQLStatement | null => { +export const deActivateSystemUserSQL = (userId: number): SQLStatement | null => { defaultLog.debug({ label: 'deActivateSystemUserSQL', message: 'params' }); if (!userId) { @@ -228,3 +231,105 @@ export const addSystemUserSQL = ( return sqlStatement; }; + +/** + * SQL query to activate a system user. Does nothing is the system user is already active. + * + * @param {number} systemUserId + * @return {*} {(SQLStatement | null)} + */ +export const activateSystemUserSQL = (systemUserId: number): SQLStatement | null => { + defaultLog.debug({ + label: 'activateSystemUserSQL', + message: 'activateSystemUserSQL', + systemUserId + }); + + if (!systemUserId) { + return null; + } + + const sqlStatement = SQL` + UPDATE + system_user + SET + record_end_date = NULL + WHERE + system_user_id = ${systemUserId} + AND + record_end_date IS NOT NULL + RETURNING + *; + `; + + defaultLog.debug({ + label: 'activateSystemUserSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; + +/** + * SQL query to remove all system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deleteAllSystemRolesSQL = (userId: number): SQLStatement | null => { + defaultLog.debug({ label: 'deleteSystemRolesSQL', message: 'params' }); + + if (!userId) { + return null; + } + + const sqlStatement = SQL` + DELETE FROM + system_user_role + WHERE + system_user_id = ${userId}; + `; + + defaultLog.debug({ + label: 'deleteSystemRolesSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; + +/** + * SQL query to remove all system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deleteAllProjectRolesSQL = (userId: number): SQLStatement | null => { + defaultLog.debug({ label: 'deleteAllProjectRolesSQL', message: 'params' }); + + if (!userId) { + return null; + } + + const sqlStatement = SQL` + DELETE FROM + project_participation + WHERE + system_user_id = ${userId}; + `; + + defaultLog.debug({ + label: 'deleteAllProjectRolesSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; diff --git a/api/src/request-handlers/security/authorization.ts b/api/src/request-handlers/security/authorization.ts index 41471b3fa6..f0cfaf6489 100644 --- a/api/src/request-handlers/security/authorization.ts +++ b/api/src/request-handlers/security/authorization.ts @@ -210,13 +210,13 @@ export const authorizeBySystemRole = async ( return false; } - if (systemUserObject.user_record_end_date){ + if (systemUserObject.user_record_end_date) { //system user has an expired record return false; } // Check if the user has at least 1 of the valid roles - return userHasValidRole(authorizeSystemRoles.validSystemRoles, systemUserObject?.role_names, systemUserObject?.user_record_end_date); + return userHasValidRole(authorizeSystemRoles.validSystemRoles, systemUserObject?.role_names); }; /** @@ -287,21 +287,19 @@ export const authorizeBySystemUser = async (req: Request, connection: IDBConnect * @return {*} {boolean} true if the user has at least 1 of the valid roles or no valid roles are specified, false * otherwise */ -export const userHasValidRole = function (validRoles: string | string[], userRoles: string | string[], userExpiryDate: string): boolean { +export const userHasValidRole = function (validRoles: string | string[], userRoles: string | string[]): boolean { if (!validRoles || !validRoles.length) { return true; } - if (!Array.isArray(validRoles) && !userExpiryDate) { + if (!Array.isArray(validRoles)) { validRoles = [validRoles]; } - if (!Array.isArray(userRoles) && !userExpiryDate) { + if (!Array.isArray(userRoles)) { userRoles = [userRoles]; } - - for (const validRole of validRoles) { if (userRoles.includes(validRole)) { return true; From 85a10fa22721a50b3bb68371bfc802cd0abcf42e Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Wed, 17 Nov 2021 11:12:48 -0800 Subject: [PATCH 05/18] error dialogs and snackbar working correctly --- .../attachments/AttachmentsList.tsx | 30 +++++++++++---- app/src/constants/i18n.ts | 18 +++++++-- .../features/admin/users/ActiveUsersList.tsx | 38 ++++++++++++++++--- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/app/src/components/attachments/AttachmentsList.tsx b/app/src/components/attachments/AttachmentsList.tsx index 0cfc3f5360..09704c926e 100644 --- a/app/src/components/attachments/AttachmentsList.tsx +++ b/app/src/components/attachments/AttachmentsList.tsx @@ -35,7 +35,7 @@ import { getFormattedDate, getFormattedFileSize } from 'utils/Utils'; import { IEditReportMetaForm } from '../attachments/EditReportMetaForm'; import EditFileWithMetaDialog from '../dialog/EditFileWithMetaDialog'; import ViewFileWithMetaDialog from '../dialog/ViewFileWithMetaDialog'; -import { EditReportMetaDataI18N } from 'constants/i18n'; +import { EditReportMetaDataI18N, AttachmentsI18N } from 'constants/i18n'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import { AttachmentType } from '../../constants/attachments'; @@ -86,20 +86,20 @@ const AttachmentsList: React.FC = (props) => { const dialogContext = useContext(DialogContext); - const [errorDialogProps, setErrorDialogProps] = useState({ + const defaultErrorDialogProps = { dialogTitle: EditReportMetaDataI18N.editErrorTitle, dialogText: EditReportMetaDataI18N.editErrorText, open: false, onClose: () => { - setErrorDialogProps({ ...errorDialogProps, open: false }); + dialogContext.setErrorDialog({ open: false }); }, onOk: () => { - setErrorDialogProps({ ...errorDialogProps, open: false }); + dialogContext.setErrorDialog({ open: false }); } - }); + }; const showErrorDialog = (textDialogProps?: Partial) => { - setErrorDialogProps({ ...errorDialogProps, ...textDialogProps, open: true }); + dialogContext.setErrorDialog({ ...defaultErrorDialogProps, ...textDialogProps, open: true }); }; const defaultYesNoDialogProps = { @@ -172,7 +172,14 @@ const AttachmentsList: React.FC = (props) => { props.getAttachments(true); } catch (error) { - return error; + const apiError = error as APIError; + showErrorDialog({ + dialogTitle: AttachmentsI18N.deleteErrorTitle, + dialogText: AttachmentsI18N.deleteErrorText, + dialogErrorDetails: apiError.errors, + open: true + }); + return; } }; @@ -217,7 +224,14 @@ const AttachmentsList: React.FC = (props) => { window.open(response); } catch (error) { - return error; + const apiError = error as APIError; + showErrorDialog({ + dialogTitle: AttachmentsI18N.downloadErrorTitle, + dialogText: AttachmentsI18N.downloadErrorText, + dialogErrorDetails: apiError.errors, + open: true + }); + return; } }; diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index e1319ec42f..2e2d22b1b3 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -22,12 +22,18 @@ export const CreatePermitsI18N = { 'An error has occurred while attempting to create your permits, please try again. If the error persists, please contact your system administrator.' }; -export const UploadProjectAttachmentsI18N = { +export const AttachmentsI18N = { cancelTitle: 'Cancel Upload', cancelText: 'Are you sure you want to cancel?', - uploadErrorTitle: 'Error Uploading Project Attachments', + uploadErrorTitle: 'Error Uploading Attachments', uploadErrorText: - 'An error has occurred while attempting to upload project attachments, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to upload attachments, please try again. If the error persists, please contact your system administrator.', + deleteErrorTitle: 'Error Deleting Attachment', + deleteErrorText: + 'An error has occurred while attempting to delete attachments, please try again. If the error persists, please contact your system administrator.', + downloadErrorTitle: 'Error Downloading Attachment', + downloadErrorText: + 'An error has occurred while attempting to download an attachment, please try again. If the error persists, please contact your system administrator.' }; export const CreateProjectDraftI18N = { @@ -178,3 +184,9 @@ export const EditReportMetaDataI18N = { editErrorText: 'An error has occurred while attempting to edit your report meta data, please try again. If the error persists, please contact your system administrator.' }; + +export const DeleteSystemUserI18N = { + deleteErrorTitle: 'Error Deleting System User', + deleteErrorText: + 'An error has occurred while attempting to delete the system user, please try again. If the error persists, please contact your system administrator.' +}; diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/ActiveUsersList.tsx index ee13890ec3..11045c4f9a 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/ActiveUsersList.tsx @@ -8,14 +8,17 @@ import TableHead from '@material-ui/core/TableHead'; import TablePagination from '@material-ui/core/TablePagination'; import TableRow from '@material-ui/core/TableRow'; import Typography from '@material-ui/core/Typography'; -import { CustomMenuButton } from 'components/toolbar/ActionToolbars'; -import { IGetUserResponse } from 'interfaces/useUserApi.interface'; -import React, { useContext, useState } from 'react'; -import { handleChangeRowsPerPage, handleChangePage } from 'utils/tablePaginationUtils'; import { mdiDotsVertical, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { CustomMenuButton } from 'components/toolbar/ActionToolbars'; +import { DeleteSystemUserI18N } from 'constants/i18n'; +import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { DialogContext } from 'contexts/dialogContext'; +import { IGetUserResponse } from 'interfaces/useUserApi.interface'; +import React, { useContext, useState } from 'react'; +import { handleChangePage, handleChangeRowsPerPage } from 'utils/tablePaginationUtils'; export interface IActiveUsersListProps { activeUsers: IGetUserResponse[]; @@ -36,6 +39,26 @@ const ActiveUsersList: React.FC = (props) => { const [page, setPage] = useState(0); const dialogContext = useContext(DialogContext); + const defaultErrorDialogProps = { + dialogTitle: DeleteSystemUserI18N.deleteErrorTitle, + dialogText: DeleteSystemUserI18N.deleteErrorText, + open: false, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }; + + const showErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ ...defaultErrorDialogProps, ...textDialogProps, open: true }); + }; + + const showSnackBar = (textDialogProps?: Partial) => { + dialogContext.setSnackbar({ ...textDialogProps, open: true }); + }; + const handleRemoveUserClick = (row: any) => { dialogContext.setYesNoDialog({ dialogTitle: 'Remove user?', @@ -63,9 +86,12 @@ const ActiveUsersList: React.FC = (props) => { try { await biohubApi.user.deleteSystemUser(user.id); + showSnackBar({ snackbarText: 'Success! User removed', open: true }); + props.getUsers(true); } catch (error) { - return error; + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, dialogErrorDetails: apiError.errors, open: true }); } }; From bc6d08d8a41047aa3999861f89ccd951bc302f41 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Wed, 17 Nov 2021 12:05:24 -0800 Subject: [PATCH 06/18] dialog improvements --- app/src/components/dialog/YesNoDialog.tsx | 42 +++++++++++++++---- .../features/admin/users/ActiveUsersList.tsx | 13 +++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/src/components/dialog/YesNoDialog.tsx b/app/src/components/dialog/YesNoDialog.tsx index dc4e649ad1..7ca7f258a0 100644 --- a/app/src/components/dialog/YesNoDialog.tsx +++ b/app/src/components/dialog/YesNoDialog.tsx @@ -1,12 +1,19 @@ -import Button from '@material-ui/core/Button'; +import Button, { ButtonProps } from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; -import React from 'react'; +import React, { ReactNode } from 'react'; export interface IYesNoDialogProps { + /** + * optional component to render underneath the dialog text. + * + * @type {ReactNode} + * @memberof IYesNoDialogProps + */ + dialogContent?: ReactNode; /** * The dialog window title text. * @@ -64,12 +71,20 @@ export interface IYesNoDialogProps { noButtonLabel?: string; /** - * The no button label. + * Optional yes-button props * - * @type {string} + * @type {Partial} + * @memberof IYesNoDialogProps + */ + yesButtonProps?: Partial; + + /** + * Optional no-button props + * + * @type {Partial} * @memberof IYesNoDialogProps */ - yesButtonColor?: string; + noButtonProps?: Partial; } /** @@ -92,13 +107,26 @@ const YesNoDialog: React.FC = (props) => { aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description"> {props.dialogTitle} - + {/* {props.dialogText} + */} + + + {props.dialogText && {props.dialogText}} + {props.dialogContent && props.dialogContent} + - + diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/ActiveUsersList.tsx index 11045c4f9a..560d7550e7 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/ActiveUsersList.tsx @@ -62,9 +62,20 @@ const ActiveUsersList: React.FC = (props) => { const handleRemoveUserClick = (row: any) => { dialogContext.setYesNoDialog({ dialogTitle: 'Remove user?', - dialogText: `Removing ${row.user_identifier} will remove their access to this application and all related projects. Are you sure you want to proceed?`, + dialogContent: ( + <> + + Removing {row.user_identifier} will revoke their access to this application and all related + projects. + + + Are you sure you want to proceed? + + + ), yesButtonLabel: 'Remove User', noButtonLabel: 'Cancel', + yesButtonProps: { color: 'secondary' }, onClose: () => { dialogContext.setYesNoDialog({ open: false }); }, From 6eb30276583ffe1b0121a154ca66bf5d44388c1b Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Wed, 17 Nov 2021 12:15:59 -0800 Subject: [PATCH 07/18] updated snapshots --- .../attachments/__snapshots__/DropZone.test.tsx.snap | 1 - .../__snapshots__/ProjectLocationForm.test.tsx.snap | 3 --- .../surveys/__snapshots__/CreateSurveyPage.test.tsx.snap | 1 - .../components/__snapshots__/StudyAreaForm.test.tsx.snap | 3 --- .../surveys/view/__snapshots__/SurveyPage.test.tsx.snap | 4 ++-- .../utils/__snapshots__/ProjectStepComponents.test.tsx.snap | 1 - 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap b/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap index c3c8e165e9..b5a69802fa 100644 --- a/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap +++ b/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap @@ -28,7 +28,6 @@ exports[`DropZone matches the snapshot 1`] = ` viewBox="0 0 24 24" > diff --git a/app/src/features/projects/components/__snapshots__/ProjectLocationForm.test.tsx.snap b/app/src/features/projects/components/__snapshots__/ProjectLocationForm.test.tsx.snap index 440d3d0368..1d6d7c591c 100644 --- a/app/src/features/projects/components/__snapshots__/ProjectLocationForm.test.tsx.snap +++ b/app/src/features/projects/components/__snapshots__/ProjectLocationForm.test.tsx.snap @@ -92,7 +92,6 @@ exports[`ProjectLocationForm renders correctly with default empty values 1`] = ` viewBox="0 0 24 24" > @@ -541,7 +540,6 @@ exports[`ProjectLocationForm renders correctly with errors on fields 1`] = ` viewBox="0 0 24 24" > @@ -1017,7 +1015,6 @@ exports[`ProjectLocationForm renders correctly with existing location values 1`] viewBox="0 0 24 24" > diff --git a/app/src/features/surveys/__snapshots__/CreateSurveyPage.test.tsx.snap b/app/src/features/surveys/__snapshots__/CreateSurveyPage.test.tsx.snap index 530a42ddf0..9859fd8766 100644 --- a/app/src/features/surveys/__snapshots__/CreateSurveyPage.test.tsx.snap +++ b/app/src/features/surveys/__snapshots__/CreateSurveyPage.test.tsx.snap @@ -1104,7 +1104,6 @@ exports[`CreateSurveyPage renders correctly when codes and project data are load viewBox="0 0 24 24" > diff --git a/app/src/features/surveys/components/__snapshots__/StudyAreaForm.test.tsx.snap b/app/src/features/surveys/components/__snapshots__/StudyAreaForm.test.tsx.snap index 1426a09fef..e8f6a7792c 100644 --- a/app/src/features/surveys/components/__snapshots__/StudyAreaForm.test.tsx.snap +++ b/app/src/features/surveys/components/__snapshots__/StudyAreaForm.test.tsx.snap @@ -97,7 +97,6 @@ exports[`Study Area Form renders correctly the empty component correctly 1`] = ` viewBox="0 0 24 24" > @@ -541,7 +540,6 @@ exports[`Study Area Form renders correctly the filled component correctly 1`] = viewBox="0 0 24 24" > @@ -1026,7 +1024,6 @@ exports[`Study Area Form renders correctly when errors exist 1`] = ` viewBox="0 0 24 24" > diff --git a/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap b/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap index 0a8e237382..3cd8f554e7 100644 --- a/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap +++ b/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap @@ -196,7 +196,7 @@ exports[`SurveyPage renders correctly with no end date 1`] = ` viewBox="0 0 24 24" > @@ -504,7 +504,7 @@ exports[`SurveyPage renders survey page when survey is loaded 1`] = ` viewBox="0 0 24 24" > diff --git a/app/src/utils/__snapshots__/ProjectStepComponents.test.tsx.snap b/app/src/utils/__snapshots__/ProjectStepComponents.test.tsx.snap index fab1c8bb9b..af91efda68 100644 --- a/app/src/utils/__snapshots__/ProjectStepComponents.test.tsx.snap +++ b/app/src/utils/__snapshots__/ProjectStepComponents.test.tsx.snap @@ -1368,7 +1368,6 @@ exports[`ProjectStepComponents renders the project location 1`] = ` viewBox="0 0 24 24" > From 07849cdbd2c68a6bd152741faa660c6e94ad113d Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Wed, 17 Nov 2021 12:18:07 -0800 Subject: [PATCH 08/18] Trigger Build From 7e2c24e46d40558040a7274707623359d433a754 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Wed, 17 Nov 2021 14:26:24 -0800 Subject: [PATCH 09/18] test --- api/src/queries/users/user-queries.test.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/api/src/queries/users/user-queries.test.ts b/api/src/queries/users/user-queries.test.ts index bf53b3858a..574a6ef8d1 100644 --- a/api/src/queries/users/user-queries.test.ts +++ b/api/src/queries/users/user-queries.test.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { addSystemUserSQL, getUserByIdSQL, getUserByUserIdentifierSQL, getUserListSQL } from './user-queries'; +import { + addSystemUserSQL, + getUserByIdSQL, + getUserByUserIdentifierSQL, + getUserListSQL, + deActivateSystemUserSQL +} from './user-queries'; describe('getUserByUserIdentifierSQL', () => { it('returns null response when null userIdentifier provided', () => { @@ -69,3 +75,17 @@ describe('addSystemUserSQL', () => { expect(response).to.not.be.null; }); }); + +describe('deActivateSystemUserSQL', () => { + it('returns null response when null userIdentifier provided', () => { + const response = deActivateSystemUserSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deActivateSystemUserSQL(1); + + expect(response).to.not.be.null; + }); +}); From a336ac8f9faad766a9f0aee7825c23086850ffc1 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Thu, 18 Nov 2021 08:31:52 -0800 Subject: [PATCH 10/18] user queries tests added --- api/src/queries/users/user-queries.test.ts | 47 +++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/api/src/queries/users/user-queries.test.ts b/api/src/queries/users/user-queries.test.ts index 574a6ef8d1..1a390d522f 100644 --- a/api/src/queries/users/user-queries.test.ts +++ b/api/src/queries/users/user-queries.test.ts @@ -5,7 +5,10 @@ import { getUserByIdSQL, getUserByUserIdentifierSQL, getUserListSQL, - deActivateSystemUserSQL + deActivateSystemUserSQL, + activateSystemUserSQL, + deleteAllSystemRolesSQL, + deleteAllProjectRolesSQL } from './user-queries'; describe('getUserByUserIdentifierSQL', () => { @@ -89,3 +92,45 @@ describe('deActivateSystemUserSQL', () => { expect(response).to.not.be.null; }); }); + +describe('activateSystemUserSQL', () => { + it('returns null response when null userId provided', () => { + const response = activateSystemUserSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = activateSystemUserSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteAllSystemRolesSQL', () => { + it('returns null response when null userId provided', () => { + const response = deleteAllSystemRolesSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deleteAllSystemRolesSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteAllProjectRolesSQL', () => { + it('returns null response when null userId provided', () => { + const response = deleteAllProjectRolesSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deleteAllProjectRolesSQL(1); + + expect(response).to.not.be.null; + }); +}); From d4fead72b53d32c536ac3d1347426f7625a2a313 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Thu, 18 Nov 2021 09:25:52 -0800 Subject: [PATCH 11/18] clean up --- api/src/paths/user/{userId}/delete.ts | 4 ++-- app/src/components/dialog/YesNoDialog.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/paths/user/{userId}/delete.ts b/api/src/paths/user/{userId}/delete.ts index c704a8c762..bb9d08bd91 100644 --- a/api/src/paths/user/{userId}/delete.ts +++ b/api/src/paths/user/{userId}/delete.ts @@ -7,10 +7,10 @@ import { getDBConnection } from '../../../database/db'; import { HTTP400, HTTP500 } from '../../../errors/CustomError'; import { deActivateSystemUserSQL, + deleteAllProjectRolesSQL, deleteAllSystemRolesSQL, - deleteAllProjectRolesSQL + getUserByIdSQL } from '../../../queries/users/user-queries'; -import { getUserByIdSQL } from '../../../queries/users/user-queries'; import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; import { getLogger } from '../../../utils/logger'; diff --git a/app/src/components/dialog/YesNoDialog.tsx b/app/src/components/dialog/YesNoDialog.tsx index 7ca7f258a0..0e3e8a8bcb 100644 --- a/app/src/components/dialog/YesNoDialog.tsx +++ b/app/src/components/dialog/YesNoDialog.tsx @@ -113,7 +113,7 @@ const YesNoDialog: React.FC = (props) => { {props.dialogText && {props.dialogText}} - {props.dialogContent && props.dialogContent} + {props.dialogContent} From 59bc082ae7524772824de9b768b7e049eeb605a2 Mon Sep 17 00:00:00 2001 From: Anissa Agahchen Date: Thu, 18 Nov 2021 13:32:50 -0800 Subject: [PATCH 12/18] fixed snackbar --- app/src/components/dialog/YesNoDialog.tsx | 5 --- app/src/contexts/dialogContext.tsx | 33 ++++++++++++++----- .../features/admin/users/ActiveUsersList.tsx | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/app/src/components/dialog/YesNoDialog.tsx b/app/src/components/dialog/YesNoDialog.tsx index 0e3e8a8bcb..36ad015521 100644 --- a/app/src/components/dialog/YesNoDialog.tsx +++ b/app/src/components/dialog/YesNoDialog.tsx @@ -107,15 +107,10 @@ const YesNoDialog: React.FC = (props) => { aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description"> {props.dialogTitle} - {/* - {props.dialogText} - */} - {props.dialogText && {props.dialogText}} {props.dialogContent} - - diff --git a/app/src/components/dialog/__snapshots__/YesNoDialog.test.tsx.snap b/app/src/components/dialog/__snapshots__/YesNoDialog.test.tsx.snap index c11ea09927..3e48177691 100644 --- a/app/src/components/dialog/__snapshots__/YesNoDialog.test.tsx.snap +++ b/app/src/components/dialog/__snapshots__/YesNoDialog.test.tsx.snap @@ -62,7 +62,7 @@ exports[`EditDialog matches snapshot when open, with error message 1`] = ` class="MuiDialogActions-root MuiDialogActions-spacing" >