From b1790a666f7f7617ed864208640f94fe31c5c351 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 25 Jun 2020 17:21:42 -0400 Subject: [PATCH 1/2] Auto gen UUID field on create --- package-lock.json | 34 ++++++++++++++++++++++++++++--- package.json | 4 +++- src/loaders.ts | 27 ++++++++---------------- src/middleware/authenticate.ts | 3 +++ src/middleware/process-payload.ts | 32 ++++++++++++++--------------- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1fc71fa57..15d5616134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,13 @@ "tunnel": "0.0.6", "uuid": "^3.2.1", "xml2js": "^0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "@azure/ms-rest-nodeauth": { @@ -316,6 +323,12 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -362,6 +375,11 @@ "version": "8.10.61", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.61.tgz", "integrity": "sha512-l+zSbvT8TPRaCxL1l9cwHCb0tSqGAGcjPJFItGGYat5oCTiq1uQQKYg5m7AF1mgnEBzFXGLJ2LRmNjtreRX76Q==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -4623,6 +4641,11 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -4645,6 +4668,11 @@ "version": "6.9.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -5737,9 +5765,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" }, "v8-compile-cache": { "version": "2.1.1", diff --git a/package.json b/package.json index 50c5f6db84..0a865183ac 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/lodash": "^4.14.156", "@types/nodemailer": "^6.4.0", "@types/pino": "^6.3.0", + "@types/uuid": "^8.0.0", "copyfiles": "^2.3.0", "eslint": "^7.3.1", "eslint-plugin-prettier": "^3.1.4", @@ -85,6 +86,7 @@ "pg": "^8.2.1", "pino": "^6.3.2", "sqlite3": "^4.2.0", - "ts-node-dev": "^1.0.0-pre.49" + "ts-node-dev": "^1.0.0-pre.49", + "uuid": "^8.2.0" } } diff --git a/src/loaders.ts b/src/loaders.ts index 41a7df4cff..fdeefd22ce 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -17,26 +17,17 @@ async function getCollections(keys: string[]) { return keys.map((key) => records.find((collection) => collection.collection === key)); } -async function getFields(keys: { collection: string; field: string }[]) { - const records = await database - .select('*') - .from('directus_fields') - .whereIn( - ['collection', 'field'], - keys.map((key) => [key.collection, key.field]) - ); - - return keys.map((key) => - records.find((record) => record.collection === key.collection && record.field === key.field) - ); -} - export default function createSystemLoaders() { return { collections: new DataLoader(getCollections), - fields: new DataLoader(getFields, { - cacheKeyFn: (key: { collection: string; field: string }) => - key.collection + '__' + key.field, - }), + fieldsByCollection: new DataLoader((collections: string[]) => + database + .select('*') + .from('directus_fields') + .whereIn('collection', collections) + .then((rows) => + collections.map((collection) => rows.filter((x) => x.collection === collection)) + ) + ), }; } diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index f2c69bab29..60fbaaec29 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -4,6 +4,9 @@ import isJWT from '../utils/is-jwt'; import database from '../database'; import asyncHandler from 'express-async-handler'; +/** + * Verify the passed JWT and assign the user ID and role to `req` + */ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.token) return next(); diff --git a/src/middleware/process-payload.ts b/src/middleware/process-payload.ts index d767fa5179..dbca919bcd 100644 --- a/src/middleware/process-payload.ts +++ b/src/middleware/process-payload.ts @@ -7,34 +7,24 @@ import { RequestHandler } from 'express'; import asyncHandler from 'express-async-handler'; import { FieldInfo } from '../types/field'; import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; 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? + * Move this out of the middleware into a service */ 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 fieldsInCollection = await req.loaders.fieldsByCollection.load(req.collection); - const fieldInfoForFields = await req.loaders.fields.loadMany( - fieldsInPayload.map((field) => ({ - collection: req.collection, - field: field, - })) - ); - - const specialFields = fieldInfoForFields.filter((field) => { + const specialFields = fieldsInCollection.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); @@ -54,12 +44,20 @@ async function processField( ) { switch (field.special) { case 'hash': - return await hash(payload[field.field]); + return await genHash(payload[field.field]); + case 'uuid': + return await genUUID(operation); } } -async function hash(value: string | number) { +async function genHash(value: string | number) { return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS)); } +async function genUUID(operation: Operation) { + if (operation === 'create') { + return uuidv4(); + } +} + export default processPayload; From db6d43988dff979d1687039e4804e011bb116c24 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 25 Jun 2020 17:59:11 -0400 Subject: [PATCH 2/2] Change payload middleware to service --- package-lock.json | 5 --- package.json | 1 - src/app.ts | 2 - src/loaders.ts | 33 ---------------- src/middleware/init-loaders.ts | 14 ------- src/middleware/process-payload.ts | 63 ------------------------------- src/routes/items.ts | 9 +++-- src/services/payload.ts | 62 ++++++++++++++++++++++++++++++ src/services/users.ts | 8 +++- 9 files changed, 74 insertions(+), 123 deletions(-) delete mode 100644 src/loaders.ts delete mode 100644 src/middleware/init-loaders.ts delete mode 100644 src/middleware/process-payload.ts create mode 100644 src/services/payload.ts diff --git a/package-lock.json b/package-lock.json index 15d5616134..3f88056850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1107,11 +1107,6 @@ "assert-plus": "^1.0.0" } }, - "dataloader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz", - "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==" - }, "date-utils": { "version": "1.2.21", "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz", diff --git a/package.json b/package.json index 0a865183ac..fd57d832cf 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "atob": "^2.1.2", "bcrypt": "^5.0.0", "body-parser": "^1.19.0", - "dataloader": "^2.0.0", "dotenv": "^8.2.0", "express": "^4.17.1", "express-async-handler": "^1.1.4", diff --git a/src/app.ts b/src/app.ts index c07c0e673a..cfec9db7be 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,7 +7,6 @@ import logger from 'express-pino-logger'; import { errorHandler } from './error'; -import initLoaders from './middleware/init-loaders'; import extractToken from './middleware/extract-token'; import authenticate from './middleware/authenticate'; @@ -33,7 +32,6 @@ const app = express() .disable('x-powered-by') .use(logger()) .use(bodyParser.json()) - .use(initLoaders) .use(extractToken) .use(authenticate) .use('/activity', activityRouter) diff --git a/src/loaders.ts b/src/loaders.ts deleted file mode 100644 index fdeefd22ce..0000000000 --- a/src/loaders.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Loaders are individual DataLoader instances that can be used to query often used information more - * efficiently. This is relied on for fetching field information for example, seeing that's an - * operation we'll do often in various middleware checks - */ - -import DataLoader from 'dataloader'; -import { FieldInfo } from './types/field'; -import database from './database'; - -async function getCollections(keys: string[]) { - const records = await database - .select('*') - .from('directus_collections') - .whereIn('collection', keys); - - return keys.map((key) => records.find((collection) => collection.collection === key)); -} - -export default function createSystemLoaders() { - return { - collections: new DataLoader(getCollections), - fieldsByCollection: new DataLoader((collections: string[]) => - database - .select('*') - .from('directus_fields') - .whereIn('collection', collections) - .then((rows) => - collections.map((collection) => rows.filter((x) => x.collection === collection)) - ) - ), - }; -} diff --git a/src/middleware/init-loaders.ts b/src/middleware/init-loaders.ts deleted file mode 100644 index 5d61de7e8a..0000000000 --- a/src/middleware/init-loaders.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Sets up the data loaders for this individual request. This allows us to reuse the same system - * loaders in the other middleware and route handlers - */ - -import { RequestHandler } from 'express'; -import createSystemLoaders from '../loaders'; - -const initLoaders: RequestHandler = (req, res, next) => { - req.loaders = createSystemLoaders(); - next(); -}; - -export default initLoaders; diff --git a/src/middleware/process-payload.ts b/src/middleware/process-payload.ts deleted file mode 100644 index dbca919bcd..0000000000 --- a/src/middleware/process-payload.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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'; -import { v4 as uuidv4 } from 'uuid'; - -type Operation = 'create' | 'update'; - -/** - * @TODO - * - * Move this out of the middleware into a service - */ - -const processPayload = (operation: Operation) => { - const middleware: RequestHandler = asyncHandler(async (req, res, next) => { - const fieldsInCollection = await req.loaders.fieldsByCollection.load(req.collection); - - const specialFields = fieldsInCollection.filter((field) => { - if (field instanceof Error) return false; - return field.special !== null; - }); - - 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 genHash(payload[field.field]); - case 'uuid': - return await genUUID(operation); - } -} - -async function genHash(value: string | number) { - return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS)); -} - -async function genUUID(operation: Operation) { - if (operation === 'create') { - return uuidv4(); - } -} - -export default processPayload; diff --git a/src/routes/items.ts b/src/routes/items.ts index 1d0c4fbeec..dfecda47be 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -6,7 +6,7 @@ import validateCollection from '../middleware/validate-collection'; import validateSingleton from '../middleware/validate-singleton'; import validateQuery from '../middleware/validate-query'; import * as MetaService from '../services/meta'; -import processPayload from '../middleware/process-payload'; +import * as PayloadService from '../services/payload'; const router = express.Router(); @@ -14,9 +14,9 @@ router.post( '/:collection', validateCollection, validateSingleton, - processPayload('create'), asyncHandler(async (req, res) => { - await createItem(req.params.collection, req.body); + const payload = await PayloadService.processValues('create', req.collection, req.body); + await createItem(req.params.collection, payload); res.status(200).end(); }) ); @@ -55,7 +55,8 @@ router.patch( '/:collection/:pk', validateCollection, asyncHandler(async (req, res) => { - await updateItem(req.params.collection, req.params.pk, req.body); + const payload = await PayloadService.processValues('update', req.collection, req.body); + await updateItem(req.params.collection, req.params.pk, payload); return res.status(200).end(); }) ); diff --git a/src/services/payload.ts b/src/services/payload.ts new file mode 100644 index 0000000000..5a99765781 --- /dev/null +++ b/src/services/payload.ts @@ -0,0 +1,62 @@ +/** + * # PayloadService + * + * Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are + * handled correctly. + */ + +import { FieldInfo } from '../types/field'; +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; +import database from '../database'; +import { clone } from 'lodash'; + +/** + * Process and update all the special fields in the given payload + * + * @param collection Collection the payload goes in + * @param operation If this is on create or on update + * @param payload The actual payload itself + * @returns The updated payload + */ +export const processValues = async ( + operation: 'create' | 'update', + collection: string, + payload: Record +) => { + const processedPayload = clone(payload); + const specialFieldsInCollection = await database + .select('field', 'special') + .from('directus_fields') + .where({ collection: collection }) + .whereNotNull('special'); + + for (const field of specialFieldsInCollection) { + processedPayload[field.field] = await processField(field, processedPayload, operation); + } + + return processedPayload; +}; + +async function processField( + field: FieldInfo, + payload: Record, + operation: 'create' | 'update' +) { + switch (field.special) { + case 'hash': + return await genHash(payload[field.field]); + case 'uuid': + return await genUUID(operation); + } +} + +async function genHash(value: string | number) { + return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS)); +} + +async function genUUID(operation: 'create' | 'update') { + if (operation === 'create') { + return uuidv4(); + } +} diff --git a/src/services/users.ts b/src/services/users.ts index 6448632d4a..a2669356a9 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -5,6 +5,7 @@ import { sendInviteMail } from '../mail'; import database from '../database'; import APIError, { ErrorCode } from '../error'; import bcrypt from 'bcrypt'; +import * as PayloadService from '../services/payload'; export const createUser = async (data: Record, query?: Query) => { return await ItemsService.createItem('directus_users', data, query); @@ -27,7 +28,12 @@ export const deleteUser = async (pk: string | number) => { }; export const inviteUser = async (email: string, role: string) => { - await createUser({ email, role, status: 'invited' }); + const userPayload = await PayloadService.processValues('create', 'directus_users', { + email, + role, + status: 'invited', + }); + await createUser(userPayload); const payload = { email }; const token = jwt.sign(payload, process.env.SECRET, { expiresIn: '7d' });