Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BHBC-1949: Email notification to Users that access has been granted or denied #829

Merged
merged 10 commits into from
Oct 4, 2022
8 changes: 0 additions & 8 deletions api/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { getLogger } from '../utils/logger';

const defaultLog = getLogger('models/user');

export class UserObject {
id: number;
user_identifier: string;
Expand All @@ -10,8 +6,6 @@ export class UserObject {
role_names: string[];

constructor(obj?: any) {
defaultLog.debug({ label: 'UserObject', message: 'params', obj });

this.id = obj?.system_user_id || null;
this.user_identifier = obj?.user_identifier || null;
this.record_end_date = obj?.record_end_date || null;
Expand All @@ -27,8 +21,6 @@ export class ProjectUserObject {
project_role_names: string[];

constructor(obj?: any) {
defaultLog.debug({ label: 'ProjectUserObject', message: 'params', obj });

this.project_id = obj?.project_id || null;
this.system_user_id = obj?.system_user_id || null;
this.project_role_ids = (obj?.project_role_ids?.length && obj.project_role_ids) || [];
Expand Down
120 changes: 0 additions & 120 deletions api/src/paths/gcnotify/send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import chai, { expect } from 'chai';
import { describe } from 'mocha';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { HTTPError } from '../../errors/custom-error';
import { getRequestHandlerMocks } from '../../__mocks__/db';
import * as notify from './send';

Expand Down Expand Up @@ -43,125 +42,6 @@ describe('gcnotify', () => {
}
};

it('should throw a 400 error when no req body', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = null;

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required param: body');
}
});

it('should throw a 400 error when no recipient', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, recipient: null };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: recipient');
}
});

it('should throw a 400 error when no message', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, message: null };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: message');
}
});

it('should throw a 400 error when no message.header', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, header: null } };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: message.header');
}
});

it('should throw a 400 error when no message.body1', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, body1: null } };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: message.body1');
}
});

it('should throw a 400 error when no message.body2', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, body2: null } };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: message.body2');
}
});

it('should throw a 400 error when no message.footer', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = sampleReq.params;
mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, footer: null } };

try {
const requestHandler = notify.sendNotification();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: message.footer');
}
});

it('sends email notification and returns 200 on success', async () => {
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

Expand Down
58 changes: 12 additions & 46 deletions api/src/paths/gcnotify/send.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../constants/roles';
import { HTTP400 } from '../../errors/custom-error';
import { IgcNotifyPostReturn } from '../../models/gcnotify';
import { authorizeRequestHandler } from '../../request-handlers/security/authorization';
import { GCNotifyService } from '../../services/gcnotify-service';
import { getLogger } from '../../utils/logger';

const defaultLog = getLogger('paths/gcnotify');

const APP_HOST = process.env.APP_HOST;

export const POST: Operation = [
authorizeRequestHandler(() => {
return {
Expand Down Expand Up @@ -42,17 +43,7 @@ POST.apiDoc = {
properties: {
recipient: {
type: 'object',
oneOf: [
{
required: ['emailAddress']
},
{
required: ['phoneNumber']
},
{
required: ['userId']
}
],
required: ['emailAddress', 'userId'],
properties: {
emailAddress: {
type: 'string'
Expand Down Expand Up @@ -107,10 +98,12 @@ POST.apiDoc = {
type: 'string'
},
reference: {
type: 'string'
type: 'string',
nullable: true
},
scheduled_for: {
type: 'string'
type: 'string',
nullable: true
},
template: {
type: 'object'
Expand Down Expand Up @@ -149,35 +142,7 @@ POST.apiDoc = {
export function sendNotification(): RequestHandler {
return async (req, res) => {
const recipient = req.body?.recipient || null;
const message = req.body?.message || null;

if (!req.body) {
throw new HTTP400('Missing required param: body');
}

if (!recipient) {
throw new HTTP400('Missing required body param: recipient');
}

if (!message) {
throw new HTTP400('Missing required body param: message');
}

if (!message.header) {
throw new HTTP400('Missing required body param: message.header');
}

if (!message.body1) {
throw new HTTP400('Missing required body param: message.body1');
}

if (!message.body2) {
throw new HTTP400('Missing required body param: message.body2');
}

if (!message.footer) {
throw new HTTP400('Missing required body param: message.footer');
}
const message = { ...req.body?.message, footer: `To access the site, ${APP_HOST}` } || null;

try {
const gcnotifyService = new GCNotifyService();
Expand All @@ -191,9 +156,10 @@ export function sendNotification(): RequestHandler {
response = await gcnotifyService.sendPhoneNumberGCNotification(recipient.phoneNumber, message);
}

if (recipient.userId) {
defaultLog.error({ label: 'send gcnotify', message: 'email and sms from Id not implemented yet' });
}
//TODO: send an email or sms depending on users ID and data
// if (recipient.userId) {
// defaultLog.error({ label: 'send gcnotify', message: 'email and sms from Id not implemented yet' });
// }

return res.status(200).json(response);
} catch (error) {
Expand Down
51 changes: 49 additions & 2 deletions app/src/features/admin/users/AccessRequestList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { AdministrativeActivityStatusType } from 'constants/misc';
import { DialogContext } from 'contexts/dialogContext';
import { APIError } from 'hooks/api/useAxios';
import { useBiohubApi } from 'hooks/useBioHubApi';
import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface';
import {
IgcNotifyGenericMessage,
IgcNotifyRecipient,
IGetAccessRequestsListResponse
} from 'interfaces/useAdminApi.interface';
import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface';
import React, { useContext, useState } from 'react';
import { getFormattedDate } from 'utils/Utils';
Expand Down Expand Up @@ -53,7 +57,6 @@ const AccessRequestList: React.FC<IAccessRequestListProps> = (props) => {
const { accessRequests, codes, refresh } = props;

const classes = useStyles();

const biohubApi = useBiohubApi();

const [activeReviewDialog, setActiveReviewDialog] = useState<{
Expand Down Expand Up @@ -83,6 +86,28 @@ const AccessRequestList: React.FC<IAccessRequestListProps> = (props) => {

setActiveReviewDialog({ open: false, request: null });

try {
await biohubApi.admin.sendGCNotification(
{
emailAddress: updatedRequest.data.email,
userId: updatedRequest.id
} as IgcNotifyRecipient,
{
subject: 'SIMS: Your request for access has been approved.',
header: 'Your request for access to the Species Inventory Management System has been approved.',
body1: 'This is an automated message from the BioHub Species Inventory Management System',
body2: '',
footer: ''
} as IgcNotifyGenericMessage
KjartanE marked this conversation as resolved.
Show resolved Hide resolved
);
} catch (error) {
dialogContext.setErrorDialog({
...defaultErrorDialogProps,
open: true,
dialogErrorDetails: (error as APIError).errors
});
}

try {
await biohubApi.admin.approveAccessRequest(
updatedRequest.id,
Expand All @@ -106,6 +131,28 @@ const AccessRequestList: React.FC<IAccessRequestListProps> = (props) => {

setActiveReviewDialog({ open: false, request: null });

try {
await biohubApi.admin.sendGCNotification(
{
emailAddress: updatedRequest.data.email,
userId: updatedRequest.id
} as IgcNotifyRecipient,
KjartanE marked this conversation as resolved.
Show resolved Hide resolved
{
subject: 'SIMS: Your request for access has been denied.',
header: 'Your request for access to the Species Inventory Management System has been denied.',
body1: 'This is an automated message from the BioHub Species Inventory Management System',
body2: '',
footer: ''
} as IgcNotifyGenericMessage
);
} catch (error) {
dialogContext.setErrorDialog({
...defaultErrorDialogProps,
open: true,
dialogErrorDetails: (error as APIError).errors
});
}

try {
await biohubApi.admin.denyAccessRequest(updatedRequest.id);

Expand Down