UNPKG

@windingtree/wt-write-api

Version:

API to write data to the Winding Tree platform

299 lines (276 loc) 9.03 kB
const _ = require('lodash'); const Web3Utils = require('web3-utils'); const tv4 = require('tv4'); const tv4Formats = require('tv4-formats'); const countryCodes = require('iso-3166-1-alpha-2'); const languageCodes = require('iso-639-1'); const currencyCodes = require('currency-codes'); const timezones = require('timezones.json'); const validator = require('validator'); const PhoneNumber = require('awesome-phonenumber'); const { errors: wtJsLibsErrors } = require('@windingtree/wt-js-libs'); const descriptionSchema = require('./description-schema.json'); const ratePlansSchema = require('./rateplans-schema.json'); const availabilitySchema = require('./availability-schema.json'); const orgNameSchema = require('./orgname-schema.json'); const contactsSchema = require('./contact-schema.json'); const addressSchema = require('./address-schema.json'); const uploadersSchema = require('./uploaders-schema.json'); const walletSchema = require('./wallet-schema.json'); const { wtLibs, allowedUploaders } = require('../../config'); class ValidationError extends Error {}; const TIMEZONES = new Set(_(timezones).map('utc').flatten().value()); tv4.addFormat(tv4Formats); // We use the "date-time" and "uri" formats from this module. tv4.addFormat('country-code', (data) => { if (countryCodes.getCountry(data)) { return null; } return 'Not a valid ISO 3166-1 alpha-2 country code.'; }); tv4.addFormat('timezone', (data) => { if (TIMEZONES.has(data)) { return null; } return 'Not a valid timezone.'; }); tv4.addFormat('language-code', (data) => { if (languageCodes.validate(data)) { return null; } return 'Not a valid ISO 639-1 language code.'; }); tv4.addFormat('currency-code', (data) => { if (currencyCodes.code(data)) { return null; } return 'Not a valid ISO 4217 currency code.'; }); tv4.addFormat('email', (data) => { if (validator.isEmail(data)) { return null; } return 'Not a valid e-mail.'; }); tv4.addFormat('phone', (data) => { const pn = new PhoneNumber(data); if (pn.isValid()) { return null; } return `Invalid phone number: ${data}`; }); tv4.setErrorReporter((error, data, schema) => { // Better error messages for some common error cases. if (schema === uploadersSchema.definitions.uploader && error.code === tv4.errorCodes.ONE_OF_MISSING) { return 'Invalid uploader configuration'; } if (schema === uploadersSchema.definitions.uploader && error.code === tv4.errorCodes.ONE_OF_MULTIPLE) { return 'Only one uploader can be configured per document'; } }); /* Note: the json schemas were generated from the swagger * definition at * * https://github.com/windingtree/wiki/blob/master/hotel-data-swagger.yaml * * (version 0.0.4) using the "openapi2schema" CLI tool. */ function _validate (data, schema) { if (!tv4.validate(data, schema, false, true)) { var msg = tv4.error.message + ': ' + tv4.error.dataPath; throw new ValidationError(msg); } } /** * JSON schema can't validate uniqueness based on selected fields, we need to do custom validation. * * @param data * @param uniqueFields Fields that are supposed to make the object key (be unique together). * @params dataType Human readable data specification. */ function _checkDuplicities (data, uniqueFields, dataType) { if (data) { const keys = []; for (const item of data) { const key = []; for (const field of uniqueFields) { key.push(item[field]); } if (keys.find((k) => { return k.join(':') === key.join(':'); })) { throw new ValidationError(`Duplicit value for ${dataType}: ${key}`); } keys.push(key); } } } /** * Validate data against description json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateDescription = function (data) { _validate(data, descriptionSchema); _checkDuplicities(data.roomTypes, ['id'], 'room type'); }; /** * Validate data against organization name json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateOrgName = function (data) { _validate(data, orgNameSchema); }; /** * Validate data against contacts json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateContacts = function (data) { _validate(data, contactsSchema); }; /** * Validate data against address json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateAddress = function (data) { _validate(data, addressSchema); }; /** * Validate data against rate plans json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateRatePlans = function (data) { _validate(data, ratePlansSchema); _checkDuplicities(data, ['id'], 'rate plan'); }; /** * Validate data against availability json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateAvailability = function (data) { _validate(data, availabilitySchema); _checkDuplicities(data.roomTypes, ['roomTypeId', 'date'], 'availability'); }; /** * Validate the dataUri. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateDataUri = function (data) { // TODO expose this list in wt-js-libs const allowedProtocols = (Object.keys(wtLibs.options.offChainDataOptions.adapters)); const matchResult = data.match(/([a-zA-Z-]+):\/\//i); const protocol = matchResult ? matchResult[1] : null; if (!protocol || allowedProtocols.indexOf(protocol) === -1) { throw new ValidationError(`Not a valid URL: ${data}`); } }; /** * Validate the notifications url. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateNotifications = function (data) { // eslint-disable-next-line camelcase const opts = { require_protocol: true, require_tld: false }; if (data && !validator.isURL(data, opts)) { throw new ValidationError(`Not a valid URL: ${data}`); } }; /** * Validate the booking url. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateBooking = function (data) { // eslint-disable-next-line camelcase const opts = { protocols: ['https'], require_protocol: true, require_tld: false }; if (data && !validator.isURL(data, opts)) { throw new ValidationError(`Not a valid secure URL: ${data}`); } }; /** * Validate data using the web3 library. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateWallet = function (data) { if (!(data instanceof Object)) { // This case is not handled by the "unlock" method. throw new ValidationError('Not a valid V3 wallet'); } _validate(data, walletSchema); try { const wallet = wtLibs.createWallet(data); wallet.unlock('dummy'); } catch (err) { if (err instanceof wtJsLibsErrors.MalformedWalletError) { throw new ValidationError(err.message); } } }; /** * Validates the guarantee using the wt-js-libs by comparing * claim and signature to a guarantor field in the claim * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateGuarantee = function (guarantee) { const client = wtLibs.getTrustClueClient(); try { const decodedData = JSON.parse(Web3Utils.hexToUtf8(guarantee.claim)); client.verifySignedData(guarantee.claim, guarantee.signature, (actualSigner) => { if (Web3Utils.toChecksumAddress(decodedData.guarantor) !== actualSigner) { throw new Error(`Expected signer '${decodedData.guarantor}' does not match the recovered one '${actualSigner}'`); } } ); } catch (err) { throw new ValidationError(err.message); } }; // Check if all allowed uploaders are defined in the schema and // patch the uploaders schema to reflect allowed uploaders. for (const key of allowedUploaders) { if (!uploadersSchema.definitions[key]) { throw new Error(`Unknown uploader in 'allowedUploaders': ${key}`); } } uploadersSchema.definitions.uploader.oneOf = allowedUploaders.map((uploader) => { return { $ref: `#/definitions/${uploader}` }; }); /** * Validate uploaders against their json schema definition. * * @param {Object} data * @return {undefined} * @throws {ValidationError} When data validation fails. */ module.exports.validateUploaders = function (data) { return _validate(data, uploadersSchema); }; module.exports.ValidationError = ValidationError;