@openveo/portal
Version:
OpenVeo Portal gives access to medias exposed by OpenVeo server associated to an OpenVeo Publish plugin
818 lines (728 loc) • 21.8 kB
JavaScript
require('./processRequire.js');
const readline = require('readline');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const os = require('os');
const async = require('async');
const openVeoApi = require('@openveo/api');
const context = process.require('app/server/context.js');
const confDir = path.join(openVeoApi.fileSystem.getConfDir(), 'portal');
const exit = process.exit;
const ResourceFilter = openVeoApi.storages.ResourceFilter;
// Create a readline interface to interact with the user
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let rlSecure = false;
/**
* Secure question to not show stdout
*/
function secureQuestion(query, callback) {
rlSecure = true;
rl.question(query, (value) => {
rl.history = rl.history.slice(1);
rlSecure = false;
callback(value);
});
}
// rewrite stdout in cas of secure rl
process.stdin.on('data', (char) => {
if (rlSecure) {
const pass = Array(rl.line.length + 1).join('*');
process.stdout.write(`\x1B[2K\x1B[200D${pass}`);
}
});
/**
* Creates a random hash.
*
* @param {Number} size The length of the expected hash
* @return {String} A hash
*/
function getRandomHash(size) {
return crypto.randomBytes(256).toString('base64').slice(0, size);
}
/**
* Creates conf directory if it does not exist.
*
* @param {Function} callback Function to call when its done with :
* - **Error** An error if something went wrong, null otherwise
*/
function createConfDir(callback) {
openVeoApi.fileSystem.mkdir(confDir, callback);
}
/**
* Creates logger directory if it does not exist.
*
* @param {Function} callback Function to call when its done with :
* - **Error** An error if something went wrong, null otherwise
*/
function createLoggerDir(callback) {
const conf = require(path.join(confDir, 'loggerConf.json'));
openVeoApi.fileSystem.mkdir(path.dirname(conf.fileName), callback);
}
/**
* Creates portal configuration file if it does not exist.
*
* @param {Function} callback Function to call when its done
*/
function createConf(callback) {
const confFile = path.join(confDir, 'conf.json');
const conf = {
passwordHashKey: getRandomHash(10),
theme: 'default',
exposedFilter: [],
categoriesFilter: '',
privateFilter: [],
publicFilter: [],
cache: {
filterTTL: 10,
videoTTL: 10
},
useDialog: false
};
async.series([
// Test if portal configuration exists
// If configuration file already exists, nothing is done
(callback) => {
fs.access(confFile, (error) => {
if (!error)
callback(new Error(`${confFile} already exists\n`));
else
callback();
});
},
// Ask for password hash key
(callback) => {
rl.question(
`Enter a secret key used to encrypt users passwords (default: ${conf.passwordHashKey}) :\n`,
(answer) => {
if (answer) conf.passwordHashKey = answer;
callback();
}
);
},
// Ask for theme
(callback) => {
rl.question(`Enter the name of the theme to use (default: "${conf.theme}") :\n`, (answer) => {
conf.theme = answer || conf.theme;
callback();
});
},
// Ask for Piwik
(callback) => {
rl.question(`If you want to embed your analytic HTML code,
you will need to define it in assets/themes/${conf.theme}/analytics.html
(press enter to continue)\n`, (answer) => {
callback();
});
},
// Ask for category filter
(callback) => {
rl.question('Enter the taxonomy ID that the user could filter (default: none) :\n', (answer) => {
conf.categoriesFilter = answer || conf.categoriesFilter;
callback();
});
},
// Ask for available exposedFilter
(callback) => {
const ask = () => {
rl.question('Enter the video properties ID that the user could filter ' +
'(Enter empty value to skip or finish) :\n', (answer) => {
if (answer) {
conf.exposedFilter.push(answer);
ask();
} else {
callback();
}
});
};
ask();
},
// Ask for public group filter
(callback) => {
const ask = () => {
rl.question(`Enter the video group IDs that anonymous user will see.
(Enter empty value to skip or finish):\n`, (answer) => {
if (answer) {
conf.publicFilter.push(answer);
ask();
} else {
callback();
}
});
};
ask();
},
// Ask for private group filter
(callback) => {
const ask = () => {
const alert = conf.privateFilter.length ? '' :
'If you do not specify any group, results will not change even if the user is connected';
rl.question(`Enter the video group IDs that authentified user will see. ${alert}
(Enter empty value to skip or finish):\n`, (answer) => {
if (answer) {
conf.privateFilter.push(answer);
ask();
} else {
callback();
}
});
};
ask();
},
// Ask for cache filter TTL
(callback) => {
rl.question('Enter the TTL of cache filter in seconds (default:' + conf.cache.filterTTL + ') :\n', (answer) => {
conf.cache.filterTTL = parseInt(answer || conf.cache.filterTTL);
callback();
});
},
// Ask for cache video TTL
(callback) => {
rl.question('Enter the TTL of cache video in seconds (default:' + conf.cache.videoTTL + ') :\n', (answer) => {
conf.cache.videoTTL = parseInt(answer || conf.cache.videoTTL);
callback();
});
},
// Ask for UI to open video
(callback) => {
rl.question('Do you want to open video in a popin dialog (n/Y) :\n', (answer) => {
if (answer === 'n') conf.useDialog = false;
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
fs.writeFile(confFile, JSON.stringify(conf, null, '\t'), {encoding: 'utf8'}, callback);
});
}
/**
* Creates database configuration file if it does not exist.
*
* @param {Function} callback Function to call when its done
*/
function createDatabaseConf(callback) {
const confFile = path.join(confDir, 'databaseConf.json');
const conf = {
type: 'mongodb',
host: 'localhost',
port: 27017
};
async.series([
// Test if database configuration exists
// If configuration file already exists, nothing is done
(callback) => {
fs.access(confFile, (error) => {
if (!error)
callback(new Error(`${confFile} already exists\n`));
else
callback();
});
},
// Ask for database host
(callback) => {
rl.question(`Enter database host (default: ${conf.host}) :\n`, (answer) => {
conf.host = answer || conf.host;
callback();
});
},
// Ask for database port
(callback) => {
rl.question(`Enter database port (default: ${conf.port}) :\n`, (answer) => {
conf.port = parseInt(answer || conf.port);
callback();
});
},
// Ask for database name
(callback) => {
rl.question('Enter database name :\n', (answer) => {
conf.database = answer || '';
callback();
});
},
// Ask for database user name
(callback) => {
rl.question('Enter database user name :\n', (answer) => {
conf.username = answer || '';
callback();
});
},
// Ask for database user password
(callback) => {
secureQuestion('Enter database user password :\n', (answer) => {
conf.password = answer || '';
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
fs.writeFile(confFile, JSON.stringify(conf, null, '\t'), {encoding: 'utf8'}, callback);
});
}
/**
* Creates loggers configuration file if it does not exist.
*
* @param {Function} callback Function to call when its done
*/
function createLoggerConf(callback) {
const confFile = path.join(confDir, 'loggerConf.json');
const conf = {
level: 'info',
maxFileSize: 104857600,
maxFiles: 2,
console: false
};
async.series([
// Test if logger configuration exists
// If configuration file already exists, nothing is done
(callback) => {
fs.access(confFile, (error) => {
if (!error)
callback(new Error(`${confFile} already exists\n`));
else
callback();
});
},
// Ask for log directory path
(callback) => {
const defaultPath = path.join(os.tmpdir(), 'openveo', 'logs');
rl.question(`Enter OpenVeo Portal logs directory (default: ${defaultPath}) :\n`, (answer) => {
conf.fileName = path.join((answer || defaultPath), 'openveo-portal.log').replace(/\\/g, '/');
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
fs.writeFile(confFile, JSON.stringify(conf, null, '\t'), {encoding: 'utf8'}, callback);
});
}
/**
* Asks the user for cas configuration.
*
* @param {Function} The function to call when its done with :
* - **Error** An error if something went wrong
* - **Object** The cas configuration
*/
function askForCasAuthConf(callback) {
const conf = {
version: '3'
};
async.series([
// Ask for application service registered in cas server
(callback) => {
rl.question('Enter the application service registered in cas server :\n', (answer) => {
conf.service = answer;
callback();
});
},
// Enter the url of the cas server
(callback) => {
rl.question('Enter the url of the cas server :\n', (answer) => {
conf.url = answer;
callback();
});
},
// Ask for authentication
(callback) => {
rl.question(`Enter the version of the cas protocol to use to connect to the cas server ? (default: \
${conf.version}) :\n`, (answer) => {
conf.version = answer || conf.version;
callback();
});
},
// Ask for certificate
(callback) => {
rl.question('Enter the complete path to the cas certificate:\n', (answer) => {
if (answer) conf.certificate = answer;
callback();
});
},
// Ask for attribute holding the user group
(callback) => {
rl.question('Enter the name of the attribute holding the user group:\n', (answer) => {
if (answer) conf.userGroupAttribute = answer;
callback();
});
},
// Ask for attribute holding the user id
(callback) => {
rl.question('Enter the name of the attribute holding the user unique id:\n', (answer) => {
if (answer) conf.userIdAttribute = answer;
callback();
});
},
// Ask for attribute holding the user name
(callback) => {
rl.question('Enter the name of the attribute holding the user name id:\n', (answer) => {
if (answer) conf.userNameAttribute = answer;
callback();
});
},
// Ask for attribute holding the user name
(callback) => {
rl.question('Enter the name of the attribute holding the user email:\n', (answer) => {
if (answer) conf.userEmailAttribute = answer;
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
callback(conf);
});
}
/**
* Asks the user for LDAP configuration.
*
* @param {Function} The function to call when its done with:
* - **Error** An error if something went wrong
* - **Object** The LDAP configuration
*/
function askForLdapAuthConf(callback) {
const conf = {
searchFilter: '(&(objectclass=person)(cn={{username}}))'
};
async.series([
// LDAP server url
(callback) => {
rl.question('Enter the url of the LDAP server:\n', (answer) => {
conf.url = answer;
callback();
});
},
// Bind attribute
(callback) => {
rl.question('Enter the entry attribute to bind to when connecting to LDAP server: (default: dn)\n', (answer) => {
if (answer) conf.bindAttribute = answer;
callback();
});
},
// Entry id
(callback) => {
rl.question('Enter the entry id to use to connect to LDAP server:\n', (answer) => {
conf.bindDn = answer;
callback();
});
},
// Entry password
(callback) => {
secureQuestion('Enter the entry password:\n', (answer) => {
conf.bindPassword = answer;
callback();
});
},
// Search base
(callback) => {
rl.question('Enter the search base when looking for users:\n', (answer) => {
conf.searchBase = answer;
callback();
});
},
// Search scope
(callback) => {
rl.question('Enter the search scope when looking for users: (default: sub)\n', (answer) => {
if (answer) conf.searchScope = answer;
callback();
});
},
// Search filter
(callback) => {
rl.question(`Enter the search filter to find a user: (default: ${conf.searchFilter})\n`, (answer) => {
conf.searchFilter = answer || conf.searchFilter;
callback();
});
},
// Group attribute
(callback) => {
rl.question('Enter the name of the attribute holding the user group:\n', (answer) => {
if (answer) conf.userGroupAttribute = answer;
callback();
});
},
// User id attribute
(callback) => {
rl.question('Enter the name of the attribute holding the user unique id:\n', (answer) => {
if (answer) conf.userIdAttribute = answer;
callback();
});
},
// User name attribute
(callback) => {
rl.question('Enter the name of the attribute holding the user name:\n', (answer) => {
if (answer) conf.userNameAttribute = answer;
callback();
});
},
// User name attribute
(callback) => {
rl.question('Enter the name of the attribute holding the user email:\n', (answer) => {
if (answer) conf.userEmailAttribute = answer;
callback();
});
},
// Certificate
(callback) => {
rl.question('Enter the complete path to the LDAP certificate:\n', (answer) => {
if (answer) conf.certificate = answer;
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
callback(conf);
});
}
/**
* Creates server configuration file if it does not exist.
*
* @param {Function} callback Function to call when its done
*/
function createServerConf(callback) {
let authConf;
const confFile = path.join(confDir, 'serverConf.json');
const conf = {
port: 3003
};
async.series([
// Test if server configuration exists
// If configuration file already exists, nothing is done
(callback) => {
fs.access(confFile, (error) => {
if (!error)
callback(new Error(`${confFile} already exists\n`));
else
callback();
});
},
// Ask for log session hash
(callback) => {
const hash = getRandomHash(40);
secureQuestion(`Enter a hash to secure HTTP sessions (default: ${hash}) :\n`, (answer) => {
conf.sessionSecret = answer || hash;
callback();
});
},
// Ask for HTTP server port
(callback) => {
rl.question(`Enter OpenVeo Portal server port (default: ${conf.port}) :\n`, (answer) => {
conf.port = parseInt(answer || conf.port);
callback();
});
},
// Ask for authentication
(callback) => {
rl.question('Do you want to add authentication to the portal ? (y/N) :\n', (answer) => {
if (answer === 'y') authConf = {};
callback();
});
},
// Local
(callback) => {
if (!authConf) return callback();
rl.question('Do you want to add local authentication to the portal? (y/N) :\n', (answer) => {
if (answer === 'y') authConf.local = true;
callback();
});
},
// CAS
(callback) => {
if (!authConf) return callback();
rl.question('Do you want to configure authentication using CAS ? (y/N) :\n', (answer) => {
if (answer === 'y') {
askForCasAuthConf((casConf) => {
authConf.cas = casConf;
callback();
});
} else {
callback();
}
});
},
// LDAP
(callback) => {
if (!authConf) return callback();
rl.question('Do you want to configure authentication using LDAP ? (y/N) :\n', (answer) => {
if (answer === 'y') {
askForLdapAuthConf((ldapConf) => {
authConf.ldapauth = ldapConf;
callback();
});
} else {
callback();
}
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else {
if (authConf) conf.auth = authConf;
fs.writeFile(confFile, JSON.stringify(conf, null, '\t'), {encoding: 'utf8'}, callback);
}
});
}
/**
* Creates portal configuration file if it does not exist.
*
* @param {Function} callback Function to call when its done
*/
function createWebservicesConf(callback) {
const confFile = path.join(confDir, 'webservicesConf.json');
const conf = {
path: 'http://127.0.0.1:3002',
certificate: ''
};
async.series([
// Test if webservices configuration exists
// If configuration file already exists, nothing is done
(callback) => {
fs.access(confFile, (error) => {
if (!error)
callback(new Error(`${confFile} already exists\n`));
else
callback();
});
},
// Ask webservices host
(callback) => {
rl.question(`Enter Webservices path (default: ${conf.path}):\n`, (answer) => {
conf.path = answer || conf.path;
callback();
});
},
// Ask certificate path
(callback) => {
rl.question(`Enter Webservices certificate path (default: ${conf.certificate}):\n`, (answer) => {
conf.certificate = answer || conf.certificate;
callback();
});
},
// Ask webservices oAuth client ID
(callback) => {
rl.question('Enter Webservices oAuth Client ID :\n', (answer) => {
conf.clientID = answer || '';
callback();
});
},
// Ask webservices oAuth secret ID
(callback) => {
secureQuestion('Enter Webservices oAuth Secret ID :\n', (answer) => {
conf.secretID = answer || '';
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
fs.writeFile(confFile, JSON.stringify(conf, null, '\t'), {encoding: 'utf8'}, callback);
});
}
/**
* Verifies connection to the database.
*
* @param {Function} callback Function to call when its done
*/
function verifyDatabaseConf(callback) {
const databaseConf = require(path.join(confDir, 'databaseConf.json'));
const db = openVeoApi.storages.factory.get(databaseConf.type, databaseConf);
db.connect((error) => {
if (error) {
process.stdout.write(`Could not connect to the database with message : ${error.message}`);
exit();
}
context.database = db;
callback();
});
}
/**
* Creates super administrator if it does not exist.
*/
function createSuperAdmin(callback) {
const UserProvider = process.require('app/server/providers/UserProvider.js');
const userProvider = new UserProvider(context.database);
const user = {
id: '0',
locked: true,
origin: openVeoApi.passport.STRATEGIES.LOCAL
};
async.series([
(callback) => {
// Verify if the super admin does not exist
userProvider.getOne(new ResourceFilter().equal('id', '0'), null, (error, user) => {
if (user)
callback(new Error('A super admin user already exists\n'));
else
callback(error);
});
},
(callback) => {
rl.question('Enter the name of the OpenVeo Portal super admin to create:\n', (answer) => {
if (!answer) return callback(new Error('Invalid name, aborting\n'));
user.name = answer;
callback();
});
},
(callback) => {
secureQuestion('Enter the password of the OpenVeo super admin to create:\n', (answer) => {
if (!answer) return callback(new Error('Invalid password, aborting\n'));
user.password = answer;
user.passwordValidate = user.password;
callback();
});
},
(callback) => {
rl.question('Enter the email of the OpenVeo Portal super admin to create:\n', (answer) => {
if (!answer || !openVeoApi.util.isEmailValid(answer)) return callback(Error('Invalid email, aborting\n'));
user.email = answer;
callback();
});
}
], (error, results) => {
if (error) {
process.stdout.write(error.message);
callback();
} else
userProvider.add([user], (error) => {
callback(error);
});
});
}
// Launch installation
async.series([
createConfDir,
createConf,
createDatabaseConf,
createLoggerConf,
createLoggerDir,
createServerConf,
createWebservicesConf,
verifyDatabaseConf,
createSuperAdmin
], (error, results) => {
if (error)
throw error;
else {
process.stdout.write('Installation complete\n');
exit();
}
});
;