Skip to content

Commit

Permalink
Allow multiple CHEFS forms to be configured
Browse files Browse the repository at this point in the history
FE can pull from multiple data sources. BE has dynamic basic auth based on the requested form ID
  • Loading branch information
kyle1morel committed Nov 27, 2023
1 parent 7620356 commit 5d07e2d
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 43 deletions.
19 changes: 16 additions & 3 deletions app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import querystring from 'querystring';
import { name as appName, version as appVersion } from './package.json';
import { DEFAULTCORS } from './src/components/constants';
import { getLogger, httpLogger } from './src/components/log';
import { getGitRevision, readIdpList } from './src/components/utils';
import { configArrayToArray, getGitRevision, readIdpList, redactSecrets } from './src/components/utils';
import v1Router from './src/routes/v1';

import type { Request, Response } from 'express';
import type { ChefsFormConfig } from './src/types/ChefsFormConfig';

const log = getLogger(module.filename);

Expand Down Expand Up @@ -69,11 +70,23 @@ appRouter.get('/api', (_req: Request, res: Response): void => {
// Frontend configuration endpoint
appRouter.get('/config', (_req: Request, res: Response, next: (err: unknown) => void): void => {
try {
// Get the CHEFS forms and redact secrets
const chefsForms = configArrayToArray<ChefsFormConfig>({ ...config.get('server.chefs.forms') }).map(
(e: ChefsFormConfig) => redactSecrets({ ...e }, ['formApiKey'])
);

// Config is immutable so have to rebuild it to inject the redacted forms
res.status(200).json({
...config.get('frontend'),
...{
...config.get('frontend'),
chefs: {
...(config.get('frontend.chefs') as any),

Check failure on line 83 in app/app.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (16.x)

Unexpected any. Specify a different type

Check failure on line 83 in app/app.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected any. Specify a different type

Check failure on line 83 in app/app.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected any. Specify a different type
forms: chefsForms
}
},
gitRev: state.gitRev,
idpList: readIdpList(),
version: process.env.npm_package_version
version: appVersion
});
} catch (err) {
next(err);
Expand Down
25 changes: 25 additions & 0 deletions app/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { join } from 'path';
import { getLogger } from './log';
const log = getLogger(module.filename);

/**
* @function configArrayToArray
* Converts a config array to a useable array type
* @returns {object[]} The converted array
*/
export function configArrayToArray<T>(arr: any): Array<T> {

Check failure on line 12 in app/src/components/utils.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (16.x)

Unexpected any. Specify a different type

Check failure on line 12 in app/src/components/utils.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected any. Specify a different type

Check failure on line 12 in app/src/components/utils.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected any. Specify a different type
return Object.keys(arr).map((key: string) => arr[key]);
}

/**
* @function getGitRevision
* Gets the current git revision hash
Expand Down Expand Up @@ -74,3 +83,19 @@ export function readIdpList(): object[] {

return idpList;
}

/**
* @function redactSecrets
* Sanitizes objects by replacing sensitive data with a REDACTED string value
* @param {object} data An arbitrary object
* @param {string[]} fields An array of field strings to sanitize on
* @returns {object} An arbitrary object with specified secret fields marked as redacted
*/
export function redactSecrets(data: { [key: string]: unknown }, fields: Array<string>): unknown {
if (fields && Array.isArray(fields) && fields.length) {
fields.forEach((field) => {
if (data[field]) data[field] = 'REDACTED';
});
}
return data;
}
2 changes: 1 addition & 1 deletion app/src/controllers/chefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const controller = {

getSubmission: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await chefsService.getSubmission(req.params.formSubmissionId);
const response = await chefsService.getSubmission(req.query.formId as string, req.params.formSubmissionId);
res.status(200).send(response);
} catch (e: unknown) {
next(e);
Expand Down
43 changes: 43 additions & 0 deletions app/src/middleware/requireChefsFormId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @ts-expect-error api-problem lacks a defined interface; code still works fine
import Problem from 'api-problem';

import config from 'config';

import { configArrayToArray } from '../components/utils';

import type { NextFunction, Request, Response } from 'express';
import type { ChefsFormConfig } from '../types/ChefsFormConfig';

/**
* @function requireChefsFormId
* Rejects the request if there is no form ID present in the request
* or if the given Form ID is not configured
* @param {object} req Express request object
* @param {object} _res Express response object
* @param {function} next The next callback function
* @returns {function} Express middleware function
* @throws The error encountered upon failure
*/
export const requireChefsFormId = (req: Request, _res: Response, next: NextFunction) => {
const params = { ...req.query, ...req.params };

if (!params.formId) {
throw new Problem(400, {
detail: 'Form ID not present in request.',
instance: req.originalUrl
});
}

if (
!configArrayToArray<ChefsFormConfig>({ ...config.get('server.chefs.forms') }).some(
(x: ChefsFormConfig) => x.formId === (params.formId as string)
)
) {
throw new Problem(412, {
detail: 'Form not present or misconfigured.',
instance: req.originalUrl
});
}

next();
};
44 changes: 31 additions & 13 deletions app/src/routes/v1/chefs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';
import { chefsController } from '../../controllers';
import { requireChefsFormId } from '../../middleware/requireChefsFormId';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';

import type { NextFunction, Request, Response } from 'express';
Expand All @@ -13,33 +14,50 @@ router.get('/forms/:formId/export', (req: Request, res: Response, next: NextFunc
});

// Form submissions endpoint
router.get('/forms/:formId/submissions', (req: Request, res: Response, next: NextFunction): void => {
chefsController.getFormSubmissions(req, res, next);
});
router.get(
'/forms/:formId/submissions',
requireChefsFormId,
(req: Request, res: Response, next: NextFunction): void => {
chefsController.getFormSubmissions(req, res, next);
}
);

// Published version endpoint
router.get('/forms/:formId/version', (req: Request, res: Response, next: NextFunction): void => {
router.get('/forms/:formId/version', requireChefsFormId, (req: Request, res: Response, next: NextFunction): void => {
chefsController.getPublishedVersion(req, res, next);
});

// Submission endpoint
router.get('/submission/:formSubmissionId', (req: Request, res: Response, next: NextFunction): void => {
chefsController.getSubmission(req, res, next);
});
router.get(
'/submission/:formSubmissionId',
requireChefsFormId,
(req: Request, res: Response, next: NextFunction): void => {
chefsController.getSubmission(req, res, next);
}
);

// Version endpoint
router.get('/forms/:formId/versions/:versionId', (req: Request, res: Response, next: NextFunction): void => {
chefsController.getVersion(req, res, next);
});
router.get(
'/forms/:formId/versions/:versionId',
requireChefsFormId,
(req: Request, res: Response, next: NextFunction): void => {
chefsController.getVersion(req, res, next);
}
);

// Version fields endpoint
router.get('/forms/:formId/versions/:versionId/fields', (req: Request, res: Response, next: NextFunction): void => {
chefsController.getVersionFields(req, res, next);
});
router.get(
'/forms/:formId/versions/:versionId/fields',
requireChefsFormId,
(req: Request, res: Response, next: NextFunction): void => {
chefsController.getVersionFields(req, res, next);
}
);

// Version submissions endpoint
router.get(
'/forms/:formId/versions/:versionId/submissions',
requireChefsFormId,
(req: Request, res: Response, next: NextFunction): void => {
chefsController.getVersionSubmissions(req, res, next);
}
Expand Down
26 changes: 17 additions & 9 deletions app/src/services/chefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import axios from 'axios';
import config from 'config';

import { configArrayToArray } from '../components/utils';

import type { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import type { ChefsFormConfig } from '../types/ChefsFormConfig';

/**
* @function chefsAxios
Expand All @@ -19,7 +22,10 @@ function chefsAxios(options: AxiosRequestConfig = {}): AxiosInstance {

instance.interceptors.request.use(
async (cfg: InternalAxiosRequestConfig) => {
cfg.auth = { username: config.get('frontend.chefs.formId'), password: config.get('frontend.chefs.formApiKey') };
const chefsForm = configArrayToArray<ChefsFormConfig>({ ...config.get('server.chefs.forms') }).find(
(x: ChefsFormConfig) => x.formId === cfg.params.formId
);
if (chefsForm) cfg.auth = { username: chefsForm.formId, password: chefsForm.formApiKey };
return Promise.resolve(cfg);
},
(error: Error) => {
Expand All @@ -33,7 +39,7 @@ function chefsAxios(options: AxiosRequestConfig = {}): AxiosInstance {
const service = {
exportSubmissions: async (formId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/export`);
const response = await chefsAxios().get(`forms/${formId}/export`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
Expand All @@ -42,7 +48,7 @@ const service = {

getFormSubmissions: async (formId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/submissions`);
const response = await chefsAxios().get(`forms/${formId}/submissions`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
Expand All @@ -51,16 +57,16 @@ const service = {

getPublishedVersion: async (formId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/version`);
const response = await chefsAxios().get(`forms/${formId}/version`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
}
},

getSubmission: async (formSubmissionId: string) => {
getSubmission: async (formId: string, formSubmissionId: string) => {
try {
const response = await chefsAxios().get(`submissions/${formSubmissionId}`);
const response = await chefsAxios().get(`submissions/${formSubmissionId}`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
Expand All @@ -69,7 +75,7 @@ const service = {

getVersion: async (formId: string, versionId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}`);
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
Expand All @@ -78,7 +84,7 @@ const service = {

getVersionFields: async (formId: string, versionId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}/fields`);
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}/fields`, { params: { formId } });
return response.data;
} catch (e: unknown) {
throw e;
Expand All @@ -87,7 +93,9 @@ const service = {

getVersionSubmissions: async (formId: string, versionId: string) => {
try {
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}/submissions`);
const response = await chefsAxios().get(`forms/${formId}/versions/${versionId}/submissions`, {
params: { formId }
});
return response.data;
} catch (e: unknown) {
throw e;
Expand Down
5 changes: 5 additions & 0 deletions app/src/types/ChefsFormConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type ChefsFormConfig = {
name: string;
formId: string;
formApiKey: string;
};
5 changes: 1 addition & 4 deletions frontend/src/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@ const { getIsAuthenticated } = storeToRefs(useAuthStore());
<li class="mr-2">
<router-link :to="{ name: RouteNames.HOME }">Home</router-link>
</li>
<li class="mr-2">
<router-link :to="{ name: RouteNames.STYLINGS }">Stylings</router-link>
</li>
<li
v-if="getIsAuthenticated"
class="mr-2"
>
<router-link :to="{ name: RouteNames.SUBMISSIONS }">Submissions</router-link>
<router-link :to="{ name: RouteNames.INITIATIVES }">Initiatives</router-link>
</li>
</ol>
</template>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/HomeView.vue'),
meta: { title: 'Home' }
},
{
path: '/initiatives',
name: RouteNames.INITIATIVES,
component: () => import('../views/InitiativesView.vue'),
meta: { title: 'Initiatives' }
},
{
path: '/stylings',
name: RouteNames.STYLINGS,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/services/chefsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export default {
* @function getSubmission
* @returns {Promise} An axios response
*/
getSubmission(formSubmissionId: string) {
return appAxios().get(`chefs/submission/${formSubmissionId}`);
getSubmission(formId: string, formSubmissionId: string) {
return appAxios().get(`chefs/submission/${formSubmissionId}`, { params: { formId } });
},

/**
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const RouteNames = Object.freeze({
DEVELOPER: 'developer',
FORBIDDEN: 'forbidden',
HOME: 'home',
INITIATIVES: 'initiatives',
LOGIN: 'login',
LOGOUT: 'logout',
SUBMISSION: 'submission',
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/views/InitiativesView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { Button } from '@/lib/primevue';
import { RouteNames } from '@/utils/constants';
</script>

<template>
<router-link :to="{ name: RouteNames.SUBMISSIONS }">
<Button>Housing</Button>
</router-link>
</template>
5 changes: 3 additions & 2 deletions frontend/src/views/SubmissionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import type { Ref } from 'vue';
// Props
type Props = {
submissionId: any;
formId: string;
submissionId: string;
};
const props = withDefaults(defineProps<Props>(), {});
Expand All @@ -24,7 +25,7 @@ const submission: Ref<any | undefined> = ref(undefined);
// Actions
onMounted(async () => {
submission.value = (await chefsService.getSubmission(props.submissionId)).data.submission;
submission.value = (await chefsService.getSubmission(props.formId, props.submissionId)).data.submission;
});
</script>

Expand Down
Loading

0 comments on commit 5d07e2d

Please sign in to comment.