Merge pull request #27 from directus/uuid

UUID
This commit is contained in:
Rijk van Zanten
2020-06-25 17:59:42 -04:00
committed by GitHub
10 changed files with 111 additions and 138 deletions

39
package-lock.json generated
View File

@@ -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=="
}
}
},
@@ -1089,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",
@@ -4623,6 +4636,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 +4663,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 +5760,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",

View File

@@ -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",
@@ -65,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",
@@ -85,6 +85,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"
}
}

View File

@@ -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)

View File

@@ -1,42 +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));
}
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<FieldInfo>((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,
}),
};
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -1,65 +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';
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<string, any>,
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;

View File

@@ -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();
})
);

62
src/services/payload.ts Normal file
View File

@@ -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<string, any>
) => {
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<string, any>,
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();
}
}

View File

@@ -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<string, any>, 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' });