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

Rebase to main #7

Closed
wants to merge 8 commits into from
8 changes: 1 addition & 7 deletions .github/actions/build-push-container/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,11 @@ inputs:
app_chefs_geo_address_apiurl:
description: Proxy URL to BC Geo Address API URL
required: true
default: "../api/v1/bcgeoaddress/address"
app_chefs_advance_geo_address_apiurl:
description: Proxy URL to BC Geo Address API URL for advance search
required: true
default: "../api/v1/bcgeoaddress/advance/address"
ref:
description: The checkout ref id
required: false
default: ''
default: ""
pr_number:
description: Pull request number
required: false
Expand Down Expand Up @@ -98,7 +94,6 @@ runs:
VITE_CHEFSTOURURL: ${{ inputs.app_chefstoururl }}
VITE_FRONTEND_BASEPATH: ${{ inputs.route_path }}
VITE_CHEFS_GEO_ADDRESS_APIURL: ${{ inputs.app_chefs_geo_address_apiurl }}
VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL: ${{ inputs.app_chefs_advance_geo_address_apiurl }}
VITE_BC_GEO_ADDRESS_APIURL: ${{ inputs.app_bc_geo_address_apiurl }}
ENV_PATH: ./app/frontend/.env
shell: bash
Expand All @@ -109,7 +104,6 @@ runs:
echo VITE_HOWTOURL=$VITE_HOWTOURL >> $ENV_PATH
echo VITE_CHEFSTOURURL=$VITE_CHEFSTOURURL >> $ENV_PATH
echo VITE_CHEFS_GEO_ADDRESS_APIURL=$VITE_CHEFS_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL=$VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_BC_GEO_ADDRESS_APIURL=$VITE_BC_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_FRONTEND_BASEPATH=$VITE_FRONTEND_BASEPATH >> $ENV_PATH

Expand Down
7 changes: 4 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
{
"cwd": "${workspaceFolder}/app/frontend",
"env": {
"VITE_TITLE": "Common Hosted Forms - Local",
"VITE_CHEFS_GEO_ADDRESS_APIURL": "http://localhost:8080/app/api/v1/bcgeoaddress/advance/address",
"VITE_CHEFSTOURURL": "https://www.youtube.com/embed/obOhyYusMjM",
"VITE_CONTACT": "[email protected]",
"VITE_FRONTEND_BASEPATH": "/app",
"VITE_CHEFSTOURURL": "https://www.youtube.com/embed/obOhyYusMjM",
"VITE_HOWTOURL": "https://www.youtube.com/playlist?list=PL9CV_8JBQHirsQAShw45PZeU1CkU88Q53"
"VITE_HOWTOURL": "https://www.youtube.com/playlist?list=PL9CV_8JBQHirsQAShw45PZeU1CkU88Q53",
"VITE_TITLE": "Common Hosted Forms - Local"
},
"name": "CHEFS Frontend",
"outputCapture": "std",
Expand Down
20 changes: 10 additions & 10 deletions app/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 33 additions & 15 deletions app/src/components/errorToProblem.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@ const Problem = require('api-problem');

const log = require('./log')(module.filename);

module.exports = function (service, e) {
if (e.response) {
// Handle raw data
let data;
if (typeof e.response.data === 'string' || e.response.data instanceof String) {
data = JSON.parse(e.response.data);
} else {
data = e.response.data;
}
/**
* Try to convert response data to JSON, but failing that just return it as-is.
*
* @param {*} data the data to attempt to parse into JSON.
* @returns an object if data is JSON, otherwise data itself
*/
const _parseResponseData = (data) => {
let parsedData;

try {
parsedData = JSON.parse(data);
} catch (error) {
// Syntax Error: It's not valid JSON.
parsedData = data;
}

return parsedData;
};

module.exports = function (service, error) {
if (error.response) {
const data = _parseResponseData(error.response.data);

log.error(`Error from ${service}: status = ${error.response.status}, data: ${JSON.stringify(data)}`, error);

log.error(`Error from ${service}: status = ${e.response.status}, data : ${JSON.stringify(data)}`, e);
// Validation Error
if (e.response.status === 422) {
throw new Problem(e.response.status, {
if (error.response.status === 422) {
throw new Problem(error.response.status, {
detail: data.detail,
errors: data.errors,
});
}

// Something else happened but there's a response
throw new Problem(e.response.status, { detail: e.response.data.toString() });
throw new Problem(error.response.status, { detail: error.response.data.toString() });
} else {
log.error(`Unknown error calling ${service}: ${e.message}`, e);
throw new Problem(502, `Unknown ${service} Error`, { detail: e.message });
log.error(`Unknown error calling ${service}: ${error.message}`, error);

throw new Problem(502, `Unknown ${service} Error`, {
detail: error.message,
});
}
};
46 changes: 40 additions & 6 deletions app/src/forms/proxy/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const jwtService = require('../../components/jwtService');
const axios = require('axios');
const { ExternalAPIStatuses } = require('../common/constants');
const Problem = require('api-problem');
const ProxyServiceError = require('./error');
const { NotFoundError } = require('objection');
const log = require('../../components/log')(module.filename);

module.exports = {
generateProxyHeaders: async (req, res, next) => {
Expand All @@ -19,22 +22,53 @@ module.exports = {
const proxyHeaderInfo = await service.readProxyHeaders(req.headers);
// find the specified external api configuration...
const extAPI = await service.getExternalAPI(req.headers, proxyHeaderInfo);
if (extAPI.code != ExternalAPIStatuses.APPROVED) {
throw new Problem(407, 'External API has not been approved by CHEFS.');
}
// add path to endpoint url if included in headers...
const extUrl = service.createExternalAPIUrl(req.headers, extAPI.endpointUrl);
// build list of request headers based on configuration...
const extHeaders = await service.createExternalAPIHeaders(extAPI, proxyHeaderInfo);
// check for approval before we call it..
if (extAPI.code != ExternalAPIStatuses.APPROVED) {
throw new Problem(407, 'External API has not been approved by CHEFS.');
}
let axiosInstance = axios.create({
headers: extHeaders,
});
// call the external api
const { data } = await axiosInstance.get(extUrl);
const { data, status } = await axiosInstance.get(extUrl).catch(function (err) {
let message = err.message;
if (err.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
log.warn(`Error returned from the external API': ${message}`);
} else if (err.request) {
message = 'External API call made, no response received.';
log.warn(message);
} else {
// Something happened in setting up the request that triggered an Error
log.warn(`Error setting up the external API request: ${message}`);
}
// send a bad gateway, the message should contain the real status
throw new Problem(502, message);
});
// if all good return data
res.status(200).json(data);
res.status(status).json(data);
} catch (error) {
next(error);
if (error instanceof ProxyServiceError) {
// making an assumption that the form component making this call
// has not been setup correctly yet.
// formio components will call as soon as the URL is entered while designing.
// calls will fire before the designer has added the headers.
log.warn(error.message);
// send back status 400 Bad Request
res.sendStatus(400);
} else if (error instanceof NotFoundError) {
// may have created formio component before adding the External API config.
log.warn('External API configuration does not exist.');
// send back status 400 Bad Request
res.sendStatus(400);
} else {
next(error);
}
}
},
};
8 changes: 8 additions & 0 deletions app/src/forms/proxy/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ProxyServiceError extends Error {
constructor(message) {
super(message);
this.name = 'ProxyServiceError';
}
}

module.exports = ProxyServiceError;
20 changes: 12 additions & 8 deletions app/src/forms/proxy/service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { encryptionService } = require('../../components/encryptionService');
const jwtService = require('../../components/jwtService');

const ProxyServiceError = require('./error');
const { ExternalAPI } = require('../../forms/common/models');

const headerValue = (headers, key) => {
Expand All @@ -21,7 +21,7 @@ const trimTrailingSlashes = (str) => str.replace(/\/+$/g, '');
const service = {
generateProxyHeaders: async (payload, currentUser, token) => {
if (!payload || !currentUser || !currentUser.idp) {
throw new Error('Cannot generate proxy headers with missing or incomplete parameters');
throw new ProxyServiceError('Cannot generate proxy headers with missing or incomplete parameters');
}

const headerData = {
Expand Down Expand Up @@ -51,16 +51,20 @@ const service = {
const data = JSON.parse(decryptedHeaderData);
return data;
} catch (error) {
throw new Error(`Could not decrypt proxy headers: ${error.message}`);
throw new ProxyServiceError(`Could not decrypt proxy headers: ${error.message}`);
}
} else {
throw new Error('Proxy headers not found');
throw new ProxyServiceError('X-CHEFS-PROXY-DATA headers not found or empty.');
}
},
getExternalAPI: async (headers, proxyHeaderInfo) => {
const externalApiName = headerValue(headers, 'X-CHEFS-EXTERNAL-API-NAME');
const externalAPI = await ExternalAPI.query().modify('findByFormIdAndName', proxyHeaderInfo['formId'], externalApiName).first().throwIfNotFound();
return externalAPI;
if (externalApiName) {
const externalAPI = await ExternalAPI.query().modify('findByFormIdAndName', proxyHeaderInfo['formId'], externalApiName).first().throwIfNotFound();
return externalAPI;
} else {
throw new ProxyServiceError('X-CHEFS-EXTERNAL-API-NAME header not found or empty.');
}
},
createExternalAPIUrl: (headers, endpointUrl) => {
//check incoming request headers for path to add to the endpoint url
Expand All @@ -78,15 +82,15 @@ const service = {
if (externalAPI.sendUserToken) {
if (!proxyHeaderInfo || !proxyHeaderInfo.token) {
// just assume that if there is no idpUserId than it isn't a userInfo object
throw new Error('Cannot create user token headers for External API without populated proxy header info token.');
throw new ProxyServiceError('Cannot create user token headers for External API without populated proxy header info token.');
}
const val = externalAPI.userTokenBearer ? `Bearer ${proxyHeaderInfo['token']}` : proxyHeaderInfo['token'];
result[externalAPI.userTokenHeader] = val;
}
if (externalAPI.sendUserInfo) {
if (!proxyHeaderInfo || !proxyHeaderInfo.idp) {
// just assume that if there is no idp than it isn't a userInfo object
throw new Error('Cannot create user headers for External API without populated proxy header info object.');
throw new ProxyServiceError('Cannot create user headers for External API without populated proxy header info object.');
}

// user information (no token)
Expand Down
21 changes: 17 additions & 4 deletions app/tests/unit/components/errorToProblem.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ const errorToProblem = require('../../../src/components/errorToProblem');
const SERVICE = 'TESTSERVICE';

describe('errorToProblem', () => {
it('should throw a 404', () => {
const error = {
response: {
data: { detail: 'detail' },
status: 404,
},
};

expect(() => errorToProblem(SERVICE, error)).toThrow('404');
});

it('should throw a 422', () => {
const e = {
const error = {
response: {
data: { detail: 'detail' },
status: 422,
},
};
expect(() => errorToProblem(SERVICE, e)).toThrow('422');

expect(() => errorToProblem(SERVICE, error)).toThrow('422');
});

it('should throw a 502', () => {
const e = {
const error = {
message: 'msg',
};
expect(() => errorToProblem(SERVICE, e)).toThrow('502');

expect(() => errorToProblem(SERVICE, error)).toThrow('502');
});
});
26 changes: 24 additions & 2 deletions app/tests/unit/forms/proxy/controller.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { getMockReq, getMockRes } = require('@jest-mock/express');
const controller = require('../../../../src/forms/proxy/controller');
const service = require('../../../../src/forms/proxy/service');
const jwtService = require('../../../../src/components/jwtService');
const { NotFoundError } = require('objection');

const bearerToken = Math.random().toString(36).substring(2);

Expand Down Expand Up @@ -138,9 +139,30 @@ describe('callExternalApi', () => {

expect(service.readProxyHeaders).toBeCalledTimes(1);
expect(service.getExternalAPI).toBeCalledTimes(1);
expect(service.createExternalAPIUrl).not.toHaveBeenCalled();
expect(service.createExternalAPIHeaders).not.toHaveBeenCalled();
expect(service.createExternalAPIUrl).toBeCalledTimes(1);
expect(service.createExternalAPIHeaders).toBeCalledTimes(1);
// this is the point where we check the status code for external api
expect(axios.get).not.toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
expect(next).toBeCalledTimes(1);
});

it('should return 400 when headers missing', async () => {
const req = getMockReq({ headers: { 'X-CHEFS-PROXY-DATA': 'encrypted blob of proxy data' } });
const { res, next } = getMockRes();

service.readProxyHeaders = jest.fn().mockReturnValue({});
service.getExternalAPI = jest.fn().mockRejectedValueOnce(new NotFoundError());
service.createExternalAPIUrl = jest.fn().mockReturnValue('http://external.api/private');
service.createExternalAPIHeaders = jest.fn().mockReturnValue({ 'X-TEST-HEADERS': 'test-headers-err' });

await controller.callExternalApi(req, res, next);

expect(service.readProxyHeaders).toBeCalledTimes(1);
expect(service.getExternalAPI).toBeCalledTimes(1);
expect(service.createExternalAPIUrl).not.toHaveBeenCalled();
expect(service.createExternalAPIHeaders).not.toHaveBeenCalled();
expect(res.sendStatus).toBeCalledWith(400);
expect(next).not.toHaveBeenCalled();
});
});
Loading
Loading