Skip to content

Commit

Permalink
Merge pull request #52 from bcgov/feature/template-email
Browse files Browse the repository at this point in the history
Implement templated roadmap functionality
  • Loading branch information
kyle1morel authored Apr 5, 2024
2 parents 5b1b22f + 106cf40 commit 8c8318b
Show file tree
Hide file tree
Showing 43 changed files with 1,465 additions and 51 deletions.
3 changes: 3 additions & 0 deletions .github/environments/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ config:
enabled: true
configMap:
FRONTEND_APIPATH: api/v1
# FRONTEND_CHES_BCC: ~
FRONTEND_COMS_APIPATH: https://coms-dev.api.gov.bc.ca/api/v1
FRONTEND_COMS_BUCKETID: 1f9e1451-c130-4804-aeb0-b78b5b109c47
FRONTEND_OIDC_AUTHORITY: https://dev.loginproxy.gov.bc.ca/auth/realms/standard
FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188
SERVER_APIPATH: /api/v1
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches-dev.api.gov.bc.ca/api/v1
SERVER_CHES_TOKENURL: https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token
SERVER_DB_PORT: "5432"
# SERVER_LOGFILE: ~
SERVER_LOGLEVEL: http
Expand Down
3 changes: 3 additions & 0 deletions .github/environments/values.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ config:
enabled: true
configMap:
FRONTEND_APIPATH: api/v1
FRONTEND_CHES_BCC: Housing.Authorizations@gov.bc.ca
FRONTEND_COMS_APIPATH: https://coms.api.gov.bc.ca/api/v1
FRONTEND_COMS_BUCKETID: 0089d041-5aab-485e-842d-8875475d0ed6
FRONTEND_OIDC_AUTHORITY: https://loginproxy.gov.bc.ca/auth/realms/standard
FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188
SERVER_APIPATH: /api/v1
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches.api.gov.bc.ca/api/v1
SERVER_CHES_TOKENURL: https://loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token
SERVER_DB_PORT: "5432"
# SERVER_LOGFILE: ~
SERVER_LOGLEVEL: http
Expand Down
3 changes: 3 additions & 0 deletions .github/environments/values.test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ config:
enabled: true
configMap:
FRONTEND_APIPATH: api/v1
# FRONTEND_CHES_BCC: ~
FRONTEND_COMS_APIPATH: https://coms-test.api.gov.bc.ca/api/v1
FRONTEND_COMS_BUCKETID: a9eabd1d-5f77-4c60-bf6b-83ffa0e21c59
FRONTEND_OIDC_AUTHORITY: https://test.loginproxy.gov.bc.ca/auth/realms/standard
FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188
SERVER_APIPATH: /api/v1
SERVER_BODYLIMIT: 30mb
SERVER_CHEFS_APIPATH: https://submit.digital.gov.bc.ca/app/api/v1
SERVER_CHES_APIPATH: https://ches-test.api.gov.bc.ca/api/v1
SERVER_CHES_TOKENURL: https://test.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token
SERVER_DB_PORT: "5432"
# SERVER_LOGFILE: ~
SERVER_LOGLEVEL: http
Expand Down
9 changes: 9 additions & 0 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"frontend": {
"apiPath": "FRONTEND_APIPATH",
"ches": {
"bcc": "FRONTEND_CHES_BCC"
},
"coms": {
"apiPath": "FRONTEND_COMS_APIPATH",
"bucketId": "FRONTEND_COMS_BUCKETID"
Expand All @@ -14,6 +17,12 @@
"server": {
"apiPath": "SERVER_APIPATH",
"bodyLimit": "SERVER_BODYLIMIT",
"ches": {
"apiPath": "SERVER_CHES_APIPATH",
"clientId": "SERVER_CHES_CLIENTID",
"clientSecret": "SERVER_CHES_CLIENTSECRET",
"tokenUrl": "SERVER_CHES_TOKENURL"
},
"chefs": {
"apiPath": "SERVER_CHEFS_APIPATH",
"forms": {
Expand Down
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as documentController } from './document';
export { default as noteController } from './note';
export { default as permitController } from './permit';
export { default as roadmapController } from './roadmap';
export { default as submissionController } from './submission';
export { default as userController } from './user';
2 changes: 1 addition & 1 deletion app/src/controllers/note.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NIL } from 'uuid';

import { noteService, userService } from '../services';
import { getCurrentIdentity } from '../components/utils';
import { noteService, userService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';

Expand Down
83 changes: 83 additions & 0 deletions app/src/controllers/roadmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NIL } from 'uuid';

import { getCurrentIdentity } from '../components/utils';
import { comsService, emailService, noteService, userService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';
import type { Email, EmailAttachment } from '../types';

const controller = {
/**
* @function send
* Send an email with the roadmap data
*/
send: async (
req: Request<never, never, { activityId: string; selectedFileIds: Array<string>; emailData: Email }>,
res: Response,
next: NextFunction
) => {
try {
if (req.body.selectedFileIds) {
const attachments: Array<EmailAttachment> = [];

const comsObjects = await comsService.getObjects(req.headers, req.body.selectedFileIds);

// Attempt to get the requested documents from COMS
// If succesful it is converted to base64 encoding and added to the attachment list
const objectPromises = req.body.selectedFileIds.map(async (id) => {
const { status, headers, data } = await comsService.getObject(req.headers, id);

if (status === 200) {
const filename = comsObjects.find((x: { id: string }) => x.id === id)?.name;
if (filename) {
attachments.push({
content: Buffer.from(data).toString('base64'),
contentType: headers['content-type'],
encoding: 'base64',
filename: filename
});
} else {
throw new Error(`Unable to obtain filename for file ${id}`);
}
}
});

await Promise.all(objectPromises);

// All succesful so attachment list is added to payload
req.body.emailData.attachments = attachments;
}

// Send the email
const { data, status } = await emailService.email(req.body.emailData);

// Add a new note on success
if (status === 201) {
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);

let noteBody = req.body.emailData.body;
if (req.body.emailData.attachments) {
noteBody += '\n\nAttachments:\n';
req.body.emailData.attachments.forEach((x) => {
noteBody += `${x.filename}\n`;
});
}

await noteService.createNote({
activityId: req.body.activityId,
note: noteBody,
noteType: 'Roadmap',
title: 'Sent roadmap',
createdAt: new Date().toISOString(),
createdBy: userId
});
}

res.status(status).json(data);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
2 changes: 1 addition & 1 deletion app/src/db/models/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type PrismaGraphNote = Prisma.noteGetPayload<typeof _noteWithGraph>;
export default {
toPrismaModel(input: Note): PrismaRelationNote {
return {
note_id: input.noteId,
note_id: input.noteId as string,
activity_id: input.activityId,
note: input.note,
note_type: input.noteType,
Expand Down
4 changes: 3 additions & 1 deletion app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import submission from './submission';
import document from './document';
import note from './note';
import permit from './permit';
import roadmap from './roadmap';
import user from './user';

const router = express.Router();
Expand All @@ -14,13 +15,14 @@ router.use(hasAccess);
// Base v1 Responder
router.get('/', (_req, res) => {
res.status(200).json({
endpoints: ['/document', '/note', '/permit', '/submission', '/user']
endpoints: ['/document', '/note', '/permit', '/roadmap', '/submission', '/user']
});
});

router.use('/document', document);
router.use('/note', note);
router.use('/permit', permit);
router.use('/roadmap', roadmap);
router.use('/submission', submission);
router.use('/user', user);

Expand Down
16 changes: 16 additions & 0 deletions app/src/routes/v1/roadmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from 'express';
import { roadmapController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { roadmapValidator } from '../../validators';

import type { NextFunction, Request, Response } from '../../interfaces/IExpress';

const router = express.Router();
router.use(requireSomeAuth);

// Send an email with the roadmap data
router.put('/', roadmapValidator.send, (req: Request, res: Response, next: NextFunction): void => {
roadmapController.send(req, res, next);
});

export default router;
53 changes: 53 additions & 0 deletions app/src/services/coms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import axios from 'axios';
import config from 'config';

import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import type { IncomingHttpHeaders } from 'http';

/**
* @function comsAxios
* Returns an Axios instance for the COMS API
* @param {AxiosRequestConfig} options Axios request config options
* @returns {AxiosInstance} An axios instance
*/
function comsAxios(options: AxiosRequestConfig = {}): AxiosInstance {
// Create axios instance
const instance = axios.create({
baseURL: config.get('frontend.coms.apiPath'),
timeout: 10000,
...options
});

return instance;
}

const service = {
/**
* @function getObject
* Get an object
* @param {IncomingHttpHeaders} incomingHeaders The request headers
* @param {string} objectId The id for the object to get
*/
async getObject(incomingHeaders: IncomingHttpHeaders, objectId: string) {
const { status, headers, data } = await comsAxios({ responseType: 'arraybuffer', headers: incomingHeaders }).get(
`/object/${objectId}`
);
return { status, headers, data };
},

/**
* @function getObjects
* Gets a list of objects
* @param {IncomingHttpHeaders} incomingHeaders The request headers
* @param {string[]} objectIds Array of object ids to get
*/
async getObjects(incomingHeaders: IncomingHttpHeaders, objectIds: Array<string>) {
const { data } = await comsAxios({ headers: incomingHeaders }).get('/object', {
params: { objectId: objectIds }
});

return data;
}
};

export default service;
93 changes: 93 additions & 0 deletions app/src/services/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import axios from 'axios';
import config from 'config';

import type { AxiosInstance } from 'axios';
import type { Email } from '../types';

/**
* @function getToken
* Gets Auth token using CHES client credentials
* @returns
*/
async function getToken() {
const response = await axios({
method: 'POST',
url: config.get('server.ches.tokenUrl'),
data: {
grant_type: 'client_credentials',
client_id: config.get('server.ches.clientId'),
client_secret: config.get('server.ches.clientSecret')
},
headers: {
'Content-type': 'application/x-www-form-urlencoded'
},
withCredentials: true
});
return response.data.access_token;
}
/**
* @function chesAxios
* Returns an Axios instance with Authorization header
* @param {AxiosRequestConfig} options Axios request config options
* @returns {AxiosInstance} An axios instance
*/
function chesAxios(): AxiosInstance {
// Create axios instance
const chesAxios = axios.create({
baseURL: config.get('server.ches.apiPath'),
timeout: 10000
});
// Add bearer token
chesAxios.interceptors.request.use(async (config) => {
const token = await getToken();
const auth = token ? `Bearer ${token}` : '';
config.headers['Authorization'] = auth;
return config;
});
return chesAxios;
}

const service = {
/**
* @function email
* Sends an email with CHES service
* @param emailData
* @returns Axios response status and data
*/
email: async (emailData: Email) => {
try {
const { data, status } = await chesAxios().post('/email', emailData, {
headers: {
'Content-Type': 'application/json'
},
maxContentLength: Infinity,
maxBodyLength: Infinity
});
return { data, status };
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
return {
data: e.response?.data.errors[0].message,
status: e.response ? e.response.status : 500
};
} else {
return {
data: 'Email error',
status: 500
};
}
}
},

/**
* @function health
* Checks CHES service health
* @returns Axios response status and data
*/
health: async () => {
const { data, status } = await chesAxios().get('/health');
return { data, status };
}
};

export default service;
4 changes: 3 additions & 1 deletion app/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { default as submissionService } from './submission';
export { default as comsService } from './coms';
export { default as documentService } from './document';
export { default as emailService } from './email';
export { default as noteService } from './note';
export { default as permitService } from './permit';
export { default as submissionService } from './submission';
export { default as userService } from './user';
16 changes: 16 additions & 0 deletions app/src/types/Email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EmailAttachment } from './EmailAttachment';

export type Email = {
bcc?: Array<string>;
bodyType: string;
body: string;
cc?: Array<string>;
delayTS?: number;
encoding?: string;
from: string;
priority?: string;
subject: string;
to: Array<string>;
tag?: string;
attachments?: Array<EmailAttachment>;
};
Loading

0 comments on commit 8c8318b

Please sign in to comment.