'use strict';
var joi = require('joi');
var _ = require('lodash');
var utils = require('yocto-utils');
/**
* Config Schema Class to manage each schema config for validation
*
* @date : 07/10/2015
* @author : ROBERT Mathieu <mathieu@yocto.re>
* @copyright : Yocto SAS, All right reserved
* @class Schema
*/
function Schema () {
/**
* Default secret key for express value
*
* @public
* @memberof Schema
* @member {String} secretKey
* @default yocto-secret-key
*/
this.secretKey = 'yocto-secret-key';
/**
* Current date value for default expires value
*
* @public
* @memberof Schema
* @member {Date} date
* @default new Date()
*/
this.date = new Date();
}
/**
* Return current express js schema defintion
*
* @return {Object} default schema for express configuration
*/
Schema.prototype.getExpress = function () {
// default schema
var schema = {
// default app rules
app : joi.object().required().keys({
name : joi.string().required().empty().min(3),
stackError : joi.boolean().default(true),
session : joi.object().default({
timeout : 50000
}).keys({
timeout : joi.number().default(50000)
}).allow('timeout')
}).allow([ 'name', 'stackError', 'session' ]),
// express rules
express : joi.object().required().keys({
// express keys for static serving folder
staticServe : joi.object().keys({
maxAge : joi.number().integer().min(0).default(0),
lastModified : joi.boolean().default(true)
}).default({}),
jsonp : joi.boolean().default(false),
prettyHtml : joi.boolean().default(true),
viewEngine : joi.string().empty().default('jade').allow('jade'),
filter : joi.object().default({
rules : 'json|text|javascript|css|html',
by : 'Content-type',
level : 9
}).keys({
rules : joi.string().default('json|text|javascript|css|html').empty(),
// for the moment only allow content type
by : joi.string().default('Content-Type').allow('Content-Type'),
level : joi.number().default(9).min(0).max(9)
}).allow([ 'rules', 'by', 'level' ]),
// json mode rules
json : joi.object().default({
inflate : true,
limit : '100kb',
strict : true,
type : 'json'
}).keys({
inflate : joi.boolean().optional().default(true),
limit : joi.string().optional().empty().default('100kb'),
strict : joi.boolean().optional().default(true),
type : joi.string().optional().empty().default('json').valid('json')
}).allow([ 'inflate', 'limit', 'strict', 'type' ]),
// url encoded rules
urlencoded : joi.object().default({
extended : true,
inflate : true,
limit : '100kb',
parameterLimit : 1000,
type : 'urlencoded'
}).keys({
extended : joi.boolean().optional().default(true),
inflate : joi.boolean().optional().default(true),
limit : joi.string().optional().empty().default('100kb'),
parameterLimit : joi.number().default(1000).min(1000),
type : joi.string().optional().empty().default('urlencoded').valid('urlencoded')
}).allow([ 'extended', 'inflate', 'limit', 'parameterLimit', 'type', 'verify' ]),
methodOverride : joi.array().min(1).unique().default([ '_method' ]).items([
joi.string().empty().default('_method').valid([
'_method',
'X-HTTP-Method',
'X-HTTP-Method-Override',
'X-Method-Override'
])
]),
// cookie parser rules
cookieParser : joi.object().default({
enable : false,
secret : 'yocto-cookie-parser-secret-key',
options : {}
}).keys({
enable : joi.boolean().default(true),
secret : joi.string().empty().default(this.secretKey),
options : joi.object().default({
path : '/',
expires : this.date,
maxAge : 0,
domain : null,
secure : true,
httpOnly : true
}).keys({
path : joi.string().empty().optional().default('/'),
expires : joi.string().empty().optional().default(this.date),
maxAge : joi.number().optional().default(0),
domain : joi.string().empty().optional().default(null),
secure : joi.boolean().optional().default(true),
httpOnly : joi.boolean().optional().default(false),
}).allow([ 'path', 'expires', 'maxAge', 'domain', 'secure', 'httpOnly' ])
}).allow([ 'enable', 'secret', 'options' ]),
// upload rules
multipart : joi.boolean().default(false),
// session rules
session : joi.object().default({ enable : false }).keys({
enable : joi.boolean().default(true),
options : joi.object().optional().keys({
cookie : joi.object().default({
path : '/',
httpOnly : false,
secure : true,
maxAge : null,
domain : null
}).keys({
path : joi.string().optional().default('/'),
httpOnly : joi.boolean().optional().default(false),
secure : joi.boolean().optional().default(true),
maxAge : joi.number().optional().default(null),
domain : joi.string().optional().default(null)
}).allow([ 'path', 'httpOnly', 'secure', 'maxAge', 'domain' ]),
secret : joi.string().optional().min(8).default(this.secretKey),
name : joi.string().optional().min(5).default('connect.sid'),
genuuid : joi.boolean().optional().default(false),
proxy : joi.boolean().optional().default(undefined),
resave : joi.boolean().optional().default(false),
saveUninitialized : joi.boolean().optional().default(true),
store : joi.object().optional().keys({
instance : joi.string().required().empty().valid('mongo'),
uri : joi.string().required().empty(),
type : joi.string().required().empty().valid([ 'mongoose', 'native', 'uri' ]),
options : joi.object().optional().keys({
ssl : joi.boolean().optional(),
sslValidate : joi.boolean().optional(),
sslCA : joi.string().optional().empty(),
sslKey : joi.string().optional().empty(),
sslCert : joi.string().optional().empty(),
checkServerIdentity : joi.boolean().optional()
}).unknown()
}).allow([ 'db', 'uri', 'type', 'options' ]),
rolling : joi.boolean().optional().default(false),
}).allow([ 'cookie', 'secret', 'name', 'genuuid',
'proxy', 'resave', 'saveUninitialized', 'rolling' ])
}).allow([ 'enable', 'options' ]),
// security rules see : https://www.npmjs.com/package/lusca
security : joi.object().default({
enable : true,
csrf : {
key : '_csrf',
secret : this.secretKey,
angular : true
},
csp : {
policy : {
'default-src' : 'none',
'script-src' : '\'self\'',
'object-src' : '\'self\'',
'style-src' : '\'self\'',
'img-src' : '\'self\'',
'media-src' : '\'self\'',
'child-src' : '\'self\'',
'font-src' : '\'self\'',
'connect-src' : '\'self\'',
'form-action' : '\'self\'',
'sandbox' : 'allow-forms allow-scripts',
'script-nonce' : '\'self\'',
'plugin-types' : '\'self\'',
'reflected-xss' : '\'self\'',
'report-uri' : '\'self\''
},
reportOnly : false
},
xframe : 'SAMEORIGIN',
p3p : '_p3p',
hsts : {
maxAge : 0,
includeSubDomains : true,
preload : true
},
xssProtection : true,
nosniff : true
}).keys({
enable : joi.boolean().default(true),
csrf : joi.object().default({
key : '_csrf',
secret : this.secretKey,
angular : true
}).keys({
key : joi.string().empty().default('_csrf'),
secret : joi.string().empty().default(this.secretKey),
angular : joi.boolean().default(true)
}),
csp : joi.object().default({
policy : {
'default-src' : 'none',
'script-src' : '\'self\'',
'object-src' : '\'self\'',
'style-src' : '\'self\'',
'img-src' : '\'self\'',
'media-src' : '\'self\'',
'child-src' : '\'self\'',
'font-src' : '\'self\'',
'connect-src' : '\'self\'',
'form-action' : '\'self\'',
// NOTE DISABLE THIS FOR CHROME ISSUE WITH FLASH
// 'sandbox' : 'allow-forms allow-scripts',
'script-nonce' : '\'self\'',
'plugin-types' : '\'self\'',
'reflected-xss' : '\'self\'',
'report-uri' : '\'self\''
},
reportOnly : false
}).keys({
policy : joi.object().default({
'default-src' : 'none',
'script-src' : '\'self\'',
'object-src' : '\'self\'',
'style-src' : '\'self\'',
'img-src' : '\'self\'',
'media-src' : '\'self\'',
'child-src' : '\'self\'',
'font-src' : '\'self\'',
'connect-src' : '\'self\'',
'form-action' : '\'self\'',
// NOTE DISABLE THIS FOR CHROME ISSUE WITH FLASH
// 'sandbox' : 'allow-forms allow-scripts',
'script-nonce' : '\'self\'',
'plugin-types' : '\'self\'',
'reflected-xss' : '\'self\'',
'report-uri' : '\'self\''
}).keys({
'default-src' : joi.string().empty().default('none'),
'script-src' : joi.string().empty().default('\'self\''),
'object-src' : joi.string().empty().default('\'self\''),
'style-src' : joi.string().empty().default('\'self\''),
'img-src' : joi.string().empty().default('\'self\''),
'media-src' : joi.string().empty().default('\'self\''),
'child-src' : joi.string().empty().default('\'self\''),
'font-src' : joi.string().empty().default('\'self\''),
'connect-src' : joi.string().empty().default('\'self\''),
'form-action' : joi.string().empty().default('\'self\''),
// NOTE DISABLE THIS FOR CHROME ISSUE WITH FLASH
// 'sandbox' : joi.string().empty().default('allow-forms allow-scripts'),
'sandbox' : joi.string().optional().empty(),
'script-nonce' : joi.string().empty().default('\'self\''),
'plugin-types' : joi.string().empty().default('\'self\''),
'reflected-xss' : joi.string().empty().default('\'self\''),
'report-uri' : joi.string().empty().default('\'self\''),
}).allow([ 'default-src', 'script-src', 'object-src', 'style-src',
'img-src', 'media-src', 'child-src', 'font-src', 'connect-src',
'form-action', 'sandbox', 'script-nonce', 'plugin-types',
'reflected-xss', 'report-uri' ]),
reportOnly : joi.boolean().default(false),
reportUri : joi.string()
}).allow('policy', 'reportOnly', 'reportUri'),
xframe : joi.string().empty().default('SAMEORIGIN'),
p3p : joi.string().empty().default('_p3p'),
hsts : joi.object().default({
maxAge : 0,
includeSubDomains : true,
preload : true
}).keys({
maxAge : joi.number().optional().default(null),
includeSubDomains : joi.boolean().default(true),
preload : joi.boolean().default(true)
}),
xssProtection : joi.boolean().default(true),
nosniff : joi.boolean().default(true)
}).allow([ 'csrf', 'csp', 'xframe', 'p3p', 'hsts', 'xssProtection', 'nosniff' ]),
// TODO : if we need to integrate vhost, we must to complete these rules
vhost : joi.object().optional().keys({
enable : joi.boolean().required().default(false),
options : joi.object().optional().keys({
url : joi.string().required().default('/'),
aliases : joi.array().items(joi.string().required().empty()),
subdomains : joi.boolean().required().default(false),
http : joi.object().optional().keys({
redirect : joi.object().required().keys({
type : joi.number(),
url : joi.string().required().empty(),
port : joi.number().required()
}).allow([ 'type', 'url', 'port' ])
}).allow('redirect')
}).allow([ 'url', 'aliases', 'subdomains', 'http' ])
}).allow([ 'enable', 'options' ])
}),
host : joi.string().default('127.0.0.1').empty(),
protocol : joi.object().default({ type : 'http' }).keys({
type : joi.string().default('http').valid([ 'http', 'https' ]),
port : joi.number().when('type', {
is : 'http',
then : joi.number().default(3000),
otherwise : joi.number().default(443)
}),
certificate : joi.object().when('type', {
is : 'http',
then : joi.optional(),
otherwise : joi.object().required().keys({
key : joi.string().required().empty(),
cert : joi.string().required().empty()
}),
})
}),
directory : joi.array().optional().min(1).unique().default([
{ models : '/' },
{ controllers : '/' },
{ views : '/' },
{ public : '/' },
{ icons : '/' }
]).items([
joi.object().keys({ models : joi.string().empty().min(1).default('/') }),
joi.object().keys({ controllers : joi.string().empty().min(1).default('/') }),
joi.object().keys({ views : joi.string().empty().min(1).default('/') }),
joi.object().keys({ public : joi.string().empty().min(1).default('/') }),
joi.object().keys({ icons : joi.string().empty().min(1).default('/') }),
joi.object().empty()
]),
encrypt : joi.object().default({ key : this.secretKey, type : 'hex' }).keys({
key : joi.string().default(this.secretKey).empty(),
type : joi.string().default('hex').valid([
'ascii',
'utf8',
'utf16le',
'ucs2',
'base64',
'binary',
'hex'
])
}),
jwt : joi.object().default({ enable : false, key : this.secretKey, ips : [] }).keys({
enable : joi.boolean().default(false),
key : joi.string().default(this.secretKey),
ips : joi.array().items(joi.string().required().empty()),
allowedRoutes : joi.array().optional().items(joi.string().required().empty()),
autoEncryptRequest : joi.boolean().optional().default(true),
autoDecryptRequest : joi.boolean().optional().default(true)
}),
cors : joi.boolean().default(false),
corsCfg : joi.object().optional().keys({
origin : joi.array().optional().items(
joi.string().required().empty()
),
methods : joi.array().optional().items(
joi.string().required().empty().valid([
'GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'
])
),
allowedHeaders : joi.array().optional().items(joi.string().required().empty()),
exposedHeaders : joi.array().optional().items(joi.string().required().empty()),
credentials : joi.boolean().optional(),
maxAge : joi.number().optional(),
preflightContinue : joi.boolean().optional()
}).allow([ 'origin', 'methods', 'allowedHeaders',
'exposedHeaders', 'credentials', 'maxAge', 'preflightContinue' ]),
redirect : joi.object().optional().keys({
www : joi.boolean().optional(),
seo : joi.array().optional().items(joi.object().required().keys({
code : joi.number().required(),
fromUrl : joi.string().required().empty(),
toUrl : joi.string().required().empty(),
queryString : joi.boolean().optional()
}))
})
};
// default statement
return schema;
};
/**
* Return current mongoose schema defintion
*
* @return {Object} default schema for mongoose configuration
*/
Schema.prototype.getMongoose = function () {
// native parser. Exclude it because underscore keys is not a good practicies and hint failed
// transformation will process at the end
var nParser = {
nativeParser : joi.boolean().default(false)
};
// replicaSet name. Exclude it because underscore keys is not a good practicies and hint failed
// transformation will process at the end
var rsName = {
rsName : joi.string().required().empty().min(3)
};
// default db Options
var dbOptions = _.extend({
serializeFunctions : joi.boolean().default(false),
raw : joi.boolean().default(false),
retryMiliSeconds : joi.number().min(0).default(5000),
numberOfRetries : joi.number().min(0).default(5)
}, utils.obj.underscoreKeys(nParser));
// default schema
var schema = joi.object().required().keys({
type : joi.string().default('mongoose').allow([ 'mongoose' ]),
uri : joi.string().required().empty(),
options : joi.object().default({}).keys({
db : joi.object().optional().keys(dbOptions),
server : joi.object().optional().keys({
poolSize : joi.number().min(5).default(5),
ssl : joi.boolean().default(false),
sslValidate : joi.boolean().default(true),
sslCA : joi.array().default(null),
sslCert : joi.string().default(null),
sslKey : joi.string().default(null),
sslPass : joi.string().default(null),
autoReconnect : joi.boolean().default(true),
socketOptions : joi.object().default({
noDelay : true,
keepAlive : 0,
connectTimeoutMS : 0,
socketTimeoutMS : 0
}).keys({
noDelay : joi.boolean().default(true),
keepAlive : joi.number().min(0).default(0),
connectTimeoutMS : joi.number().min(0).default(0),
socketTimeoutMS : joi.number().min(0).default(0)
}).allow([ 'noDelay', 'keepAlive', 'connectTimeoutMS', 'socketTimeoutMS' ])
}).allow([ 'poolSize', 'ssl', 'sslValidate', 'sslCA',
'sslCert', 'sslKey', 'sslPass', 'autoReconnect', 'socketOptions']),
replset : joi.object().optional().keys(utils.obj.underscoreKeys(rsName)),
user : joi.string().optional().empty(),
pass : joi.string().optional().empty()
}).allow([ 'db', 'server', 'replset', 'user', 'pass' ])
});
// default statement
return schema;
};
/**
* Return current passport js schema defintion
*
* @return {Object} default schema for passport configuration
*/
Schema.prototype.getPassportJs = function () {
// default validation schema
var baseSchema = {
identifier : joi.string().required().empty().default(null), // clientID
secret : joi.string().required().empty(), // sercet key
urls : joi.object().required().keys({
connect : joi.string().required().empty(), // init connection
callback : joi.string().required().empty() // system callback url
}),
db : joi.object().required().keys({
method : joi.string().required().empty()
})
};
// facebook Keys definition
var facebookKeys = _.extend(_.clone(baseSchema), {
fields : joi.array().required().items(
joi.string().valid([ 'id', 'name', 'gender', 'displayName',
'photos', 'emails', 'profileUrl'
])
)
});
// twitter Keys definition
var twitterKeys = _.clone(baseSchema);
// google Keys definition
var googleKeys = _.extend(_.clone(baseSchema), {
scope : joi.array().required().items(joi.string().required().empty()).min(1)
});
// Ad Keys definition
var activeDirectoryKeys = _.extend(_.omit(_.clone(baseSchema), [ 'identifier', 'secret' ]), {
server : joi.object().required().keys({
bindDn : joi.string().required().empty(),
bindCredentials : joi.string().required().empty(),
url : joi.string().required().empty(),
searchBase : joi.string().required().empty(),
searchFilter : joi.string().required().empty()
})
});
// standard auth
var standardAuthKeys = _.omit(_.clone(baseSchema), [ 'identifier', 'secret', 'callback' ]);
// default schema
var schema = joi.object().required().keys({
internalUrlRedirect : joi.string().required().empty(),
facebook : joi.object().optional().keys(facebookKeys),
twitter : joi.object().optional().keys(twitterKeys),
google : joi.object().optional().keys(googleKeys),
activeDirectory : joi.array().optional().items(activeDirectoryKeys),
standard : joi.array().optional().items(standardAuthKeys)
}).unknown();
// default statement
return schema;
};
/**
* Return current Render schema defintion
*
* @return {Object} default schema for passport configuration
*/
Schema.prototype.getRender = function () {
// define meta rules
var metaHttpEquivRules = joi.object().keys({
name : joi.string().required().not(null),
value : joi.string().required().not(null)
});
// setting css media rules
var cssMediaRules = joi.object().keys({
host : joi.string().uri({
scheme : [ 'http', 'https' ]
}).optional().empty(),
link : joi.string().required().not(null),
media : joi.string().required().not(null),
defer : joi.string().optional().allow('defer').not(null),
async : joi.string().optional().allow('async').not(null),
fingerprint : joi.object().optional().keys({
enable : joi.boolean().required().default(false),
key : joi.string().optional().default(this.uuid),
dateFormat : joi.string().optional().default('DD/MM/YYYY'),
qs : joi.string().optional().empty().default('v'),
limit : joi.number().optional().min(1)
}),
base64 : joi.object().optional().keys({
enable : joi.boolean().required().default(false),
qs : joi.string().optional().empty().default('r')
})
});
// setting js media rules
var jsMediaRules = joi.object().keys({
host : joi.string().uri({
scheme : [ 'http', 'https' ]
}).optional().empty(),
link : joi.string().required().not(null),
defer : joi.string().optional().allow('defer').not(null),
async : joi.string().optional().allow('async').not(null),
fingerprint : joi.object().optional().keys({
enable : joi.boolean().required().default(false),
key : joi.string().optional().default(this.uuid),
dateFormat : joi.string().optional().default('DD/MM/YYYY'),
qs : joi.string().optional().empty().default('v'),
limit : joi.number().optional().min(1)
}),
base64 : joi.object().optional().keys({
enable : joi.boolean().required().default(false),
qs : joi.string().optional().empty().default('r')
})
});
// setting up media type rules
var mediaTypeRules = {
css : joi.array().optional().min(1).items(cssMediaRules),
js : joi.array().optional().min(1).items(jsMediaRules)
};
// setting up assets rules
var assetsRules = {
header : joi.object().optional().min(1).keys(mediaTypeRules),
footer : joi.object().optional().min(1).keys(mediaTypeRules)
};
// facebook twitter keys
var facebookTwitterKeys = {
property : joi.string().required().empty(),
content : joi.string().required().empty()
};
// google keys
var googleKeys = {
rel : joi.string().required().empty(),
href : joi.string().required().empty()
};
// setting up social keys
var socialRules = {
facebook : joi.array().optional().items(facebookTwitterKeys).default([]),
twitter : joi.array().optional().items(facebookTwitterKeys).default([]),
google : joi.array().optional().items(googleKeys).default([])
};
// default statement
return joi.object().required().keys({
app : joi.object().optional().min(1).keys({
name : joi.string().required().min(3).not(null).empty()
}),
// property list
property : joi.object().optional().min(1).keys({
title : joi.string().optional().min(3).not(null),
language : joi.string().optional().length(2).not(null),
meta : joi.array().optional().min(1).items(metaHttpEquivRules),
httpEquiv : joi.array().optional().min(1).items(metaHttpEquivRules),
assets : joi.object().optional().min(1).keys(assetsRules),
mobileIcons : joi.array().optional().min(1).items(
joi.object().required().keys({
rel : joi.string().required().empty(),
sizes : joi.string().required().empty(),
href : joi.string().required().empty()
})
),
social : joi.object().optional().min(1).keys(socialRules)
})
}).allow([ 'app', 'property' ]);
};
/**
* Return current router schema defintion
*
* @return {Object} default schema for passport configuration
*/
Schema.prototype.getRouter = function () {
// default statement
return joi.object().required().keys({
routes : joi.string().required().empty().default(),
controllers : joi.string().required().empty().default()
}).allow([ 'routes', 'controllers' ]);
};
// Default export
module.exports = new (Schema)();