Skip to content

Commit

Permalink
add status code, add backend tests.
Browse files Browse the repository at this point in the history
Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed Jun 3, 2024
1 parent 4021a10 commit 9c431ef
Show file tree
Hide file tree
Showing 13 changed files with 661 additions and 34 deletions.
21 changes: 19 additions & 2 deletions app/frontend/src/components/designer/FormDesigner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ export default {
Promise.all([this.fetchForm(this.formId), this.getFormSchema()]);
}
},
mounted() {
async mounted() {
// load up headers for any External API calls
// from components (component makes calls during design phase).
await this.setProxyHeaders();
if (!this.formId) {
// We are creating a new form, so we obtain the original schema here.
this.patch.originalSchema = deepClone(this.formSchema);
Expand All @@ -227,7 +230,21 @@ export default {
'setDirtyFlag',
'getFCProactiveHelpImageUrl',
]),
async setProxyHeaders() {
try {
let response = await formService.getProxyHeaders({
formId: this.formId,
versionId: this.versionId,
});
// error checking for response
sessionStorage.setItem(
'X-CHEFS-PROXY-DATA',
response.data['X-CHEFS-PROXY-DATA']
);
} catch (error) {
// need error handling
}
},
async getFormSchema() {
try {
let res;
Expand Down
66 changes: 45 additions & 21 deletions app/src/db/migrations/20240521210143_046_external_api.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
const stamps = require('../stamps');

const { ExternalAPIStatuses } = require('../../forms/common/constants');
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
const CREATED_BY = 'migration-046';

const statusCodes = [
{ code: ExternalAPIStatuses.SUBMITTED, display: 'Submitted', createdBy: CREATED_BY },
{ code: ExternalAPIStatuses.PENDING, display: 'Pending', createdBy: CREATED_BY },
{ code: ExternalAPIStatuses.APPROVED, display: 'Approved', createdBy: CREATED_BY },
{ code: ExternalAPIStatuses.DENIED, display: 'Denied', createdBy: CREATED_BY },
];
exports.up = function (knex) {
return Promise.resolve().then(() =>
knex.schema.createTable('external_api', (table) => {
table.uuid('id').primary();
table.uuid('formId').references('id').inTable('form').notNullable().index();
table.string('name', 255).notNullable();
table.string('endpointUrl').notNullable();
return (
Promise.resolve()
.then(() =>
knex.schema.createTable('external_api_status_code', (table) => {
table.string('code').primary();
table.string('display').notNullable();
stamps(knex, table);
})
)
// seed the table
.then(() => {
return knex('external_api_status_code').insert(statusCodes);
})
.then(() =>
knex.schema.createTable('external_api', (table) => {
table.uuid('id').primary();
table.uuid('formId').references('id').inTable('form').notNullable().index();
table.string('name', 255).notNullable();
table.string('endpointUrl').notNullable();
table.string('code').references('code').inTable('external_api_status_code').notNullable().index();

table.boolean('sendApiKey').defaultTo(false);
table.string('apiKeyHeader');
table.string('apiKey');
table.boolean('sendApiKey').defaultTo(false);
table.string('apiKeyHeader');
table.string('apiKey');

table.boolean('sendUserToken').defaultTo(false);
table.string('userTokenHeader');
table.boolean('userTokenBearer').defaultTo(true);
table.boolean('sendUserToken').defaultTo(false);
table.string('userTokenHeader');
table.boolean('userTokenBearer').defaultTo(true);

table.boolean('sendUserInfo').defaultTo(false);
table.string('userInfoHeader');
table.boolean('userInfoEncrypted').defaultTo(false);
table.string('userInfoEncryptionKey');
table.string('userInfoEncryptionAlgo');
stamps(knex, table);
})
table.boolean('sendUserInfo').defaultTo(false);
table.string('userInfoHeader');
table.boolean('userInfoEncrypted').defaultTo(false);
table.string('userInfoEncryptionKey');
table.string('userInfoEncryptionAlgo');
stamps(knex, table);
})
)
);
};

Expand All @@ -36,5 +58,7 @@ exports.up = function (knex) {
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return Promise.resolve().then(() => knex.schema.dropTableIfExists('external_api'));
return Promise.resolve()
.then(() => knex.schema.dropTableIfExists('external_api'))
.then(() => knex.schema.dropTableIfExists('external_api_status_code'));
};
6 changes: 6 additions & 0 deletions app/src/forms/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,10 @@ module.exports = Object.freeze({
VIEWS_FORM_VIEW: 'views_form_view',
VIEWS_USER_SUBMISSIONS: 'views_user_submissions',
},
ExternalAPIStatuses: {
SUBMITTED: 'SUBMITTED',
PENDING: 'PENDING',
APPROVED: 'APPROVED',
DENIED: 'DENIED',
},
});
1 change: 1 addition & 0 deletions app/src/forms/common/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
FormComponentsProactiveHelp: require('./tables/formComponentsProactiveHelp'),
FormSubscription: require('./tables/formSubscription'),
ExternalAPI: require('./tables/externalAPI'),
ExternalAPIStatusCode: require('./tables/externalAPIStatusCode'),

// Views
FormSubmissionUserPermissions: require('./views/formSubmissionUserPermissions'),
Expand Down
18 changes: 17 additions & 1 deletion app/src/forms/common/models/tables/externalAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ class ExternalAPI extends Timestamps(Model) {
return 'external_api';
}

static get relationMappings() {
const ExternalAPIStatusCode = require('./externalAPIStatusCode');

return {
statusCode: {
relation: Model.HasOneRelation,
modelClass: ExternalAPIStatusCode,
join: {
from: 'external_api.code',
to: 'external_api_status_code.code',
},
},
};
}

static get modifiers() {
return {
filterFormId(query, value) {
Expand Down Expand Up @@ -63,12 +78,13 @@ class ExternalAPI extends Timestamps(Model) {
static get jsonSchema() {
return {
type: 'object',
required: ['formId', 'name', 'endpointUrl'],
required: ['formId', 'name', 'code', 'endpointUrl'],
properties: {
id: { type: 'string', pattern: Regex.UUID },
formId: { type: 'string', pattern: Regex.UUID },
name: { type: 'string', minLength: 1, maxLength: 255 },
endpointUrl: { type: 'string' },
code: { type: 'string' },
sendApiKey: { type: 'boolean', default: false },
apiKeyHeader: { type: 'string' },
apiKey: { type: 'string' },
Expand Down
24 changes: 24 additions & 0 deletions app/src/forms/common/models/tables/externalAPIStatusCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { Model } = require('objection');
const { Timestamps } = require('../mixins');
const stamps = require('../jsonSchema').stamps;

class ExternalAPIStatusCode extends Timestamps(Model) {
static get tableName() {
return 'external_api_status_code';
}

static get jsonSchema() {
return {
type: 'object',
required: ['code'],
properties: {
code: { type: 'string' },
display: { type: 'string' },
...stamps,
},
additionalProperties: false,
};
}
}

module.exports = ExternalAPIStatusCode;
2 changes: 1 addition & 1 deletion app/src/forms/form/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ module.exports = {
},
listExternalAPIs: async (req, res, next) => {
try {
const response = await service.listExternalAPIs();
const response = await service.listExternalAPIs(req.params.formId);
res.status(200).json(response);
} catch (error) {
next(error);
Expand Down
43 changes: 41 additions & 2 deletions app/src/forms/form/service.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const Problem = require('api-problem');
const { ref } = require('objection');
const { v4: uuidv4 } = require('uuid');
const { EmailTypes } = require('../common/constants');
const { EmailTypes, ExternalAPIStatuses } = require('../common/constants');
const eventService = require('../event/eventService');
const moment = require('moment');
const {
Expand All @@ -27,6 +27,7 @@ const {
const { falsey, queryUtils, checkIsFormExpired, validateScheduleObject, typeUtils } = require('../common/utils');
const { Permissions, Roles, Statuses } = require('../common/constants');
const Rolenames = [Roles.OWNER, Roles.TEAM_MANAGER, Roles.FORM_DESIGNER, Roles.SUBMISSION_REVIEWER, Roles.FORM_SUBMITTER, Roles.SUBMISSION_APPROVER];
const { ENCRYPTION_ALGORITHMS } = require('../../components/encryptionService');

const service = {
_findFileIds: (schema, data) => {
Expand Down Expand Up @@ -1050,11 +1051,44 @@ const service = {
return ExternalAPI.query().modify('filterFormId', formId);
},

validateExternalAPI: (data) => {
if (!data) {
throw new Problem(422, `'externalAPI record' cannot be empty.`);
}
if (data.sendApiKey) {
if (!data.apiKeyHeader || !data.apiKey) {
throw new Problem(422, `'apiKeyHeader' and 'apiKey' are required when 'sendApiKey' is true.`);
}
}
if (data.sendUserToken) {
if (!data.userTokenHeader) {
throw new Problem(422, `'userTokenHeader' is required when 'sendUserToken' is true.`);
}
}
if (data.sendUserInfo) {
if (data.userInfoEncrypted && !data.userInfoHeader) {
throw new Problem(422, `'userInfoHeader' is required when 'sendUserInfo' and 'userInfoEncrypted' are true.`);
}
if (data.userInfoEncrypted) {
if (!Object.values(ENCRYPTION_ALGORITHMS).includes(data.userInfoEncryptionAlgo)) {
throw new Problem(422, `'${data.userInfoEncryptionAlgo}' is not a valid Encryption Algorithm.`);
}
if (!data.userInfoEncryptionKey) {
throw new Problem(422, `'userInfoEncryptionKey' is required when 'userInfoEncrypted' is true.`);
}
}
}
},

createExternalAPI: async (formId, data, currentUser) => {
service.validateExternalAPI(data);

let trx;
let id = uuidv4();
try {
trx = await ExternalAPI.startTransaction();
// set status to SUBMITTED
data.code = ExternalAPIStatuses.SUBMITTED;
await ExternalAPI.query(trx).insert({
id: id,
...data,
Expand All @@ -1070,10 +1104,15 @@ const service = {
},

updateExternalAPI: async (formId, externalAPIId, data, currentUser) => {
service.validateExternalAPI(data);

let trx;
try {
await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).throwIfNotFound();
const existing = await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).throwIfNotFound();
trx = await ExternalAPI.startTransaction();
// let's use a different method for the administrators to update status code.
// this method should not change the status code.
data.code = existing.code;
await ExternalAPI.query(trx)
.modify('filterId', externalAPIId)
.update({
Expand Down
5 changes: 5 additions & 0 deletions app/src/forms/proxy/controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const service = require('./service');
const jwtService = require('../../components/jwtService');
const axios = require('axios');
const { ExternalAPIStatuses } = require('../common/constants');
const Problem = require('api-problem');

module.exports = {
generateProxyHeaders: async (req, res, next) => {
Expand All @@ -17,6 +19,9 @@ 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...
Expand Down
Loading

0 comments on commit 9c431ef

Please sign in to comment.