From 456f8038aab9f159c5222dc31f129c8f3fb9ea3b Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:37:52 -0400 Subject: [PATCH 1/6] Install bcrypt --- package-lock.json | 47 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 49 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6e5a56d319..cfe6a987f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,12 @@ "integrity": "sha512-8GAYQ1jDRUQkSpHzJUqXwAkYFOxuWAOGLhIR4aPd/Y/yL12Q/9m7LsKpHKlfKdNE/362Hc9wPI1Yh6opDfxVJg==", "dev": true }, + "@types/bcrypt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz", + "integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -598,6 +604,42 @@ } } }, + "bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==", + "requires": { + "node-addon-api": "^3.0.0", + "node-pre-gyp": "0.15.0" + }, + "dependencies": { + "node-pre-gyp": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3592,6 +3634,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-addon-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", + "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==" + }, "node-notifier": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", diff --git a/package.json b/package.json index c68b348a3c..d351f5c9dd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "homepage": "https://github.com/directus/api-node#readme", "devDependencies": { "@types/atob": "^2.1.2", + "@types/bcrypt": "^3.0.0", "@types/express": "^4.17.6", "@types/express-session": "^1.17.0", "@types/hapi__joi": "^17.1.2", @@ -61,6 +62,7 @@ "dependencies": { "@hapi/joi": "^17.1.1", "atob": "^2.1.2", + "bcrypt": "^5.0.0", "body-parser": "^1.19.0", "dataloader": "^2.0.0", "dotenv": "^8.2.0", From 50de63fb67cc12b885c95d0d548af150dee78928 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:38:03 -0400 Subject: [PATCH 2/6] Add salt rounds to .env --- example.env | 1 + 1 file changed, 1 insertion(+) diff --git a/example.env b/example.env index 486ab87256..614678be8a 100644 --- a/example.env +++ b/example.env @@ -20,6 +20,7 @@ DB_PASSWORD="psql1234" SECRET="abcdef" ACCESS_TOKEN_EXPIRY_TIME="15m" REFRESH_TOKEN_EXPIRY_TIME="7d" +SALT_ROUNDS=10 #################################################################################################### # SSO (oAuth) Providers From 91a23c9a148188c66a43c77d2b85aec4644f6cb9 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:38:08 -0400 Subject: [PATCH 3/6] Add typing info to loaders --- src/loaders.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/loaders.ts b/src/loaders.ts index 6687d8c4f7..45a1765f3c 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -5,6 +5,7 @@ */ import DataLoader from 'dataloader'; +import { FieldInfo } from './types/field'; import database from './database'; async function getFields(keys: { collection: string; field: string }[]) { @@ -16,7 +17,7 @@ async function getFields(keys: { collection: string; field: string }[]) { keys.map((key) => [key.collection, key.field]) ); - return keys.map((key) => + return keys.map((key) => records.find((record) => record.collection === key.collection && record.field === key.field) ); } From 9c176c095590c2eeca64faf8b446906f3b31ed3c Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:38:23 -0400 Subject: [PATCH 4/6] Add FieldInfo type --- src/types/field.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/types/field.ts diff --git a/src/types/field.ts b/src/types/field.ts new file mode 100644 index 0000000000..b4d37ac826 --- /dev/null +++ b/src/types/field.ts @@ -0,0 +1,18 @@ +export type FieldInfo = { + id: number; + collection: string; + field: string; + special: string | null; + interface: string | null; + options: Record | null; + locked: boolean; + required: boolean; + readonly: boolean; + hidden_detail: boolean; + hidden_browse: boolean; + sort: number | null; + width: string | null; + group: number | null; + note: string | null; + translation: null; +}; From 368e102906c6f89ef111009409ae7e7c677f8702 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:38:35 -0400 Subject: [PATCH 5/6] Fix express type extending --- src/types/express.d.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 6776a27682..a11eebeadc 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -2,12 +2,16 @@ * Custom properties on the req object in express */ -declare namespace Express { - export interface Request { - token?: string; - user?: string; - role?: string; - collection?: string; - loaders?: any; +import createSystemLoaders from '../loaders'; + +declare global { + namespace Express { + export interface Request { + token?: string; + user?: string; + role?: string; + collection?: string; + loaders?: ReturnType; + } } } From 21155b2db8c4da45e73da86725ee84c601afba47 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 24 Jun 2020 17:38:42 -0400 Subject: [PATCH 6/6] Add payload processing on item create --- src/middleware/process-payload.ts | 65 +++++++++++++++++++++++++++++++ src/routes/items.ts | 14 ++++--- 2 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/middleware/process-payload.ts diff --git a/src/middleware/process-payload.ts b/src/middleware/process-payload.ts new file mode 100644 index 0000000000..d767fa5179 --- /dev/null +++ b/src/middleware/process-payload.ts @@ -0,0 +1,65 @@ +/** + * Will check the fields system table for any special operations that need to be done on the field + * value, this can include hashing the value or generating a UUID + */ + +import { RequestHandler } from 'express'; +import asyncHandler from 'express-async-handler'; +import { FieldInfo } from '../types/field'; +import bcrypt from 'bcrypt'; + +type Operation = 'create' | 'update'; + +/** + * @TODO + * + * This needs a bit of extra thinking. + * - There's a difference between update / create payload processing + * - Some processing types need the whole payload (slug) + * - What happens for fields that aren't in the payload but need to be set on create? + */ + +const processPayload = (operation: Operation) => { + const middleware: RequestHandler = asyncHandler(async (req, res, next) => { + // Get the fields that have a special operation associated with them + const fieldsInPayload = Object.keys(req.body); + + const fieldInfoForFields = await req.loaders.fields.loadMany( + fieldsInPayload.map((field) => ({ + collection: req.collection, + field: field, + })) + ); + + const specialFields = fieldInfoForFields.filter((field) => { + if (field instanceof Error) return false; + return field.special !== null; + }) as FieldInfo[]; + + for (const field of specialFields) { + req.body[field.field] = await processField(req.collection, field, req.body, operation); + } + + next(); + }); + + return middleware; +}; + +async function processField( + collection: string, + field: FieldInfo, + payload: Record, + operation: Operation +) { + switch (field.special) { + case 'hash': + return await hash(payload[field.field]); + } +} + +async function hash(value: string | number) { + return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS)); +} + +export default processPayload; diff --git a/src/routes/items.ts b/src/routes/items.ts index 2a4ce6b791..acb041cbdb 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -2,15 +2,17 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import { createItem, readItems, readItem, updateItem, deleteItem } from '../services/items'; import sanitizeQuery from '../middleware/sanitize-query'; -import collectionExists from '../middleware/collection-exists'; +import validateCollection from '../middleware/validate-collection'; import validateQuery from '../middleware/validate-query'; import * as MetaService from '../services/meta'; +import processPayload from '../middleware/process-payload'; const router = express.Router(); router.post( '/:collection', - collectionExists, + validateCollection, + processPayload('create'), asyncHandler(async (req, res) => { await createItem(req.params.collection, req.body); res.status(200).end(); @@ -19,7 +21,7 @@ router.post( router.get( '/:collection', - collectionExists, + validateCollection, sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { @@ -37,7 +39,7 @@ router.get( router.get( '/:collection/:pk', - collectionExists, + validateCollection, asyncHandler(async (req, res) => { const record = await readItem(req.params.collection, req.params.pk); @@ -49,7 +51,7 @@ router.get( router.patch( '/:collection/:pk', - collectionExists, + validateCollection, asyncHandler(async (req, res) => { await updateItem(req.params.collection, req.params.pk, req.body); return res.status(200).end(); @@ -58,7 +60,7 @@ router.patch( router.delete( '/:collection/:pk', - collectionExists, + validateCollection, asyncHandler(async (req, res) => { await deleteItem(req.params.collection, req.params.pk); return res.status(200).end();