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 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", 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) ); } 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(); 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; + } } } 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; +};