Skip to content

Commit

Permalink
feat: adding JOI validiation to backend
Browse files Browse the repository at this point in the history
Installed JOI validation package
Added validation for the following endpoints:
document - createDocument, deleteDocument, listDocuments
note - createNote, listNotes
permit - createPermit, deletePermit, listPermits, updatePermit,
submission - getStatistics, getSubmission, editSubmission
user - searchUser

Validator files under src/validators
Middleware under src/middeleware/validation.ts

Modified frontend NoteCard, NoteModal, PermitModal to submit ISO dates
  for notes entries
Modified frontend SubmissionForm onSubmit JSON to match valid format
  • Loading branch information
wilwong89 committed Feb 27, 2024
1 parent 1b9daca commit 662d237
Show file tree
Hide file tree
Showing 23 changed files with 399 additions and 37 deletions.
44 changes: 44 additions & 0 deletions app/package-lock.json

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

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"express": "^4.18.2",
"express-winston": "^4.2.0",
"helmet": "^7.1.0",
"joi": "^17.12.1",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"pg": "^8.11.3",
Expand Down
6 changes: 6 additions & 0 deletions app/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ export const APPLICATION_STATUS_LIST = Object.freeze({
DELAYED: 'Delayed',
COMPLETED: 'Completed'
});

/** Types of notes */
export const NOTE_TYPE_LIST = Object.freeze({
GENERAL: 'General',
BRING_FORWARD: 'Bring Forward'
});
1 change: 0 additions & 1 deletion app/src/controllers/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const controller = {
createNote: async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);

// TODO: define body type in request
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = req.body as any;
Expand Down
32 changes: 32 additions & 0 deletions app/src/middleware/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// @ts-expect-error api-problem lacks a defined interface; code still works fine
import Problem from 'api-problem';
import type { NextFunction, Request, Response } from '../interfaces/IExpress';
// import type { NextFunction, Request, Response } from 'express';

/**
* @function validator
* Performs express request validation against a specified `schema`
* @param {object} schema An object containing Joi validation schema definitions
* @returns {function} Express middleware function
* @throws The error encountered upon failure
*/
export const validate = (schema: object) => {
return (req: Request, _res: Response, next: NextFunction) => {
const validationErrors = Object.entries(schema)
.map(([prop, def]) => {
const result = def.validate((req as any)[prop], { abortEarly: false })?.error;
return result ? [prop, result?.details] : undefined;
})
.filter((error) => !!error)
.map((x) => x as any[]);

if (Object.keys(validationErrors).length) {
throw new Problem(422, {
detail: validationErrors.flatMap((groups) => groups[1]?.map((error: any) => error?.message)).join('; '),
instance: req.originalUrl,
errors: Object.fromEntries(validationErrors)
});
} else next();
};
};
23 changes: 16 additions & 7 deletions app/src/routes/v1/document.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import express from 'express';
import { documentController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { documentValidator } from '../../validators';

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

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

router.put('/', (req: Request, res: Response, next: NextFunction): void => {
router.put('/', documentValidator.createDocument, (req: Request, res: Response, next: NextFunction): void => {
documentController.createDocument(req, res, next);
});

router.delete('/:documentId', (req: Request, res: Response, next: NextFunction): void => {
documentController.deleteDocument(req, res, next);
});
router.delete(
'/:documentId',
documentValidator.deleteDocument,
(req: Request, res: Response, next: NextFunction): void => {
documentController.deleteDocument(req, res, next);
}
);

router.get('/list/:activityId', (req: Request, res: Response, next: NextFunction): void => {
documentController.listDocuments(req, res, next);
});
router.get(
'/list/:activityId',
documentValidator.listDocuments,
(req: Request, res: Response, next: NextFunction): void => {
documentController.listDocuments(req, res, next);
}
);

export default router;
5 changes: 3 additions & 2 deletions app/src/routes/v1/note.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import express from 'express';
import { noteController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { noteValidator } from '../../validators';

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

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

// Note create endpoint
router.put('/', (req: Request, res: Response, next: NextFunction): void => {
router.put('/', noteValidator.createNote, (req: Request, res: Response, next: NextFunction): void => {
noteController.createNote(req, res, next);
});

// Note list by activity endpoint
router.get('/list/:activityId', (req: Request, res: Response, next: NextFunction): void => {
router.get('/list/:activityId', noteValidator.listNotes, (req: Request, res: Response, next: NextFunction): void => {
noteController.listNotes(req, res, next);
});

Expand Down
17 changes: 11 additions & 6 deletions app/src/routes/v1/permit.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import express from 'express';
import { permitController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { permitValidator } from '../../validators';

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

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

// Permit create endpoint
router.put('/', (req: Request, res: Response, next: NextFunction): void => {
router.put('/', permitValidator.createPermit, (req: Request, res: Response, next: NextFunction): void => {
permitController.createPermit(req, res, next);
});

// Permit update endpoint
router.put('/:permitId', (req: Request, res: Response, next: NextFunction): void => {
router.put('/:permitId', permitValidator.updatePermit, (req: Request, res: Response, next: NextFunction): void => {
permitController.updatePermit(req, res, next);
});

// Permit delete endpoint
router.delete('/:permitId', (req: Request, res: Response, next: NextFunction): void => {
router.delete('/:permitId', permitValidator.deletePermit, (req: Request, res: Response, next: NextFunction): void => {
permitController.deletePermit(req, res, next);
});

// Permit list by activity endpoint
router.get('/list/:activityId', (req: Request, res: Response, next: NextFunction): void => {
permitController.listPermits(req, res, next);
});
router.get(
'/list/:activityId',
permitValidator.listPermits,
(req: Request, res: Response, next: NextFunction): void => {
permitController.listPermits(req, res, next);
}
);

// Permit types endpoint
router.get('/types', (req: Request, res: Response, next: NextFunction): void => {
Expand Down
31 changes: 22 additions & 9 deletions app/src/routes/v1/submission.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from 'express';
import { submissionController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { submissionValidator } from '../../validators';

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

Expand All @@ -13,18 +14,30 @@ router.get('/', (req: Request, res: Response, next: NextFunction): void => {
});

// Statistics endpoint
router.get('/statistics', (req: Request, res: Response, next: NextFunction): void => {
submissionController.getStatistics(req, res, next);
});
router.get(
'/statistics',
submissionValidator.getStatistics,
(req: Request, res: Response, next: NextFunction): void => {
submissionController.getStatistics(req, res, next);
}
);

// Submission endpoint
router.get('/:activityId', (req: Request, res: Response, next: NextFunction): void => {
submissionController.getSubmission(req, res, next);
});
router.get(
'/:activityId',
submissionValidator.getSubmission,
(req: Request, res: Response, next: NextFunction): void => {
submissionController.getSubmission(req, res, next);
}
);

// Submission update endpoint
router.put('/:submissionId', (req: Request, res: Response, next: NextFunction): void => {
submissionController.updateSubmission(req, res, next);
});
router.put(
'/:submissionId',
submissionValidator.updateSubmission,
(req: Request, res: Response, next: NextFunction): void => {
submissionController.updateSubmission(req, res, next);
}
);

export default router;
3 changes: 2 additions & 1 deletion app/src/routes/v1/user.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import express from 'express';
import { userController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { userValidator } from '../../validators';

import type { NextFunction, Request, Response } from 'express';

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

// Submission endpoint
router.get('/', (req: Request, res: Response, next: NextFunction): void => {
router.get('/', userValidator.searchUsers, (req: Request, res: Response, next: NextFunction): void => {
userController.searchUsers(req, res, next);
});

Expand Down
1 change: 0 additions & 1 deletion app/src/services/permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const service = {
},
data: permit.toPrismaModel(newPermit)
});

return permit.fromPrismaModel(create);
} catch (e: unknown) {
throw e;
Expand Down
5 changes: 5 additions & 0 deletions app/src/validators/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Joi from 'joi';

export const activityId = Joi.string().min(8).max(8).required();

export const uuidv4 = Joi.string().guid({ version: 'uuidv4' });
32 changes: 32 additions & 0 deletions app/src/validators/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Joi from 'joi';

import { activityId, uuidv4 } from './common';
import { validate } from '../middleware/validation';

const schema = {
createDocument: {
body: Joi.object({
activityId: activityId,
documentId: uuidv4.required(),
filename: Joi.string().max(255).required(),
mimeType: Joi.string().max(255).required(),
length: Joi.number().required()
})
},
deleteDocument: {
params: Joi.object({
documentId: Joi.string().max(255).required()
})
},
listDocuments: {
params: Joi.object({
activityId: activityId
})
}
};

export default {
createDocument: validate(schema.createDocument),
deleteDocument: validate(schema.deleteDocument),
listDocuments: validate(schema.listDocuments)
};
5 changes: 5 additions & 0 deletions app/src/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as documentValidator } from './document';
export { default as noteValidator } from './note';
export { default as permitValidator } from './permit';
export { default as submissionValidator } from './submission';
export { default as userValidator } from './user';
26 changes: 26 additions & 0 deletions app/src/validators/note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Joi from 'joi';

import { activityId } from './common';
import { validate } from '../middleware/validation';

const schema = {
createNote: {
body: Joi.object({
createdAt: Joi.date().required(),
activityId: activityId,
note: Joi.string(),
noteType: Joi.string().max(255).required(),
title: Joi.string().max(255)
})
},
listNotes: {
params: Joi.object({
activityId: activityId
})
}
};

export default {
createNote: validate(schema.createNote),
listNotes: validate(schema.listNotes)
};
Loading

0 comments on commit 662d237

Please sign in to comment.