UNPKG

fully-api

Version:

API framework for Fully Stacked, LLC REST-ful APIs

624 lines 30.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FullyAPI = void 0; const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); // We'll create our server with the http module const Q = require('q'); const cors = require('cors'); const path_1 = __importDefault(require("path")); // Import path to control our folder structure const body_parser_1 = __importDefault(require("body-parser")); const fs_1 = __importDefault(require("fs")); const ErrorObj_1 = require("./services/ErrorObj"); const settings_1 = require("./services/settings"); const endpoints_1 = require("./services/endpoints"); const dataAccess_1 = require("./services/dataAccess"); const serviceRegistration_1 = require("./services/serviceRegistration"); const controller_1 = require("./services/controller"); const utilities_1 = require("./services/utilities"); const accessControl_1 = require("./services/accessControl"); const models_1 = require("./services/models"); const schema_1 = require("./services/schema"); // Settings File, contains DB params const nodeEnv = process.env.NODE_ENV || 'local'; var rootDir = path_1.default.dirname(require.main.filename); var configFile = rootDir + '/config/config.' + nodeEnv + '.js'; var config; try { if (fs_1.default.existsSync(configFile)) { config = require(configFile); } else { var configFile = './config/config.' + nodeEnv + '.js'; config = require(configFile); console.log('config expectd in config/ directory with main project file. Not found. Falling back to FullyAPI internal config.'); } } catch (e) { console.log('Error loading Config file: ', e); throw Error('Failed to load config file. Please make sure there is a config file config/config.NODE_ENV.(ts/js) for the environemnt you are trying to load.'); } let models; let endpoints; let dataAccess; let serviceRegistration; let utilities; let accessControl; let mainController; let errorLog; let sessionLog; let accessLog; let eventLog; const schemaControl = new schema_1.Schema(config); const settings = new settings_1.Settings(config.file_locations.settings); const useRemoteSettings = config.server_options.use_remote_settings ? typeof config.server_options.use_remote_settings == 'string' ? config.server_options.use_remote_settings.toLowerCase() == 'true' : config.server_options.use_remote_settings : false; class FullyAPI { constructor() { this.config_file = config; } startServer() { // ----------------------------------- // LAUNCH THE SERVER // ----------------------------------- const server = http_1.default .createServer(FullyAPI.prototype.app) .listen(FullyAPI.prototype.app.get('port'), function () { console.log('----------------------------------------------'); console.log('----------------------------------------------'); console.log('Express server listening on port ' + FullyAPI.prototype.app.get('port')); console.log(''); console.log('Visit http://localhost:' + FullyAPI.prototype.app.get('port') + " to access your documentation portal \nwith your default super user, or any other super user or 'admin' security role account."); if (useRemoteSettings) { settings .registerIp() .then(function (res) { console.log('Running in distributed mode'); }) .fail(function (err) { console.error('Problem registering ip'); }); } }); if (config.server_options.server_timeout != null) server.timeout = parseInt(config.server_options.server_timeout); if (config.server_options.keep_alive_timeout != null) server.keepAliveTimeout = parseInt(config.server_options.keep_alive_timeout); if (config.server_options.headers_timeout != null) server.headersTimeout = parseInt(config.server_options.headers_timeout); } configureRoutes() { // ======================================================== // SETUP ROUTE HANDLERS // ======================================================== // --------------------------------------------------------------------------------- // GETS // --------------------------------------------------------------------------------- FullyAPI.prototype.app.get('/:area/:controller/:serviceCall/:version?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'GET'); }); FullyAPI.prototype.app.get('/:area/:controller?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'GET'); }); // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // POSTS // --------------------------------------------------------------------------------- FullyAPI.prototype.app.post('/:area/:controller/:serviceCall/:version?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'POST'); }); FullyAPI.prototype.app.post('/:area/:controller?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'POST'); }); // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // PUTS // --------------------------------------------------------------------------------- FullyAPI.prototype.app.put('/:area/:controller/:serviceCall/:version?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'PUT'); }); FullyAPI.prototype.app.put('/:area/:controller?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'PUT'); }); // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // PATCH // --------------------------------------------------------------------------------- FullyAPI.prototype.app.patch('/:area/:controller/:serviceCall/:version?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'PATCH'); }); FullyAPI.prototype.app.patch('/:area/:controller?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'PATCH'); }); // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- // DELETES // --------------------------------------------------------------------------------- FullyAPI.prototype.app.delete('/:area/:controller/:serviceCall/:version?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'DELETE'); }); FullyAPI.prototype.app.delete('/:area/:controller?', function (req, res) { FullyAPI.prototype.requestPipeline(req, res, 'DELETE'); }); // --------------------------------------------------------------------------------- // --------------------------------------------------------------------------------- FullyAPI.prototype.app.get('*', function (req, res) { res.status(404).send({ Error: 'Route/File Not Found' }); }); FullyAPI.prototype.app.use(function (err, req, res, next) { if (req.xhr) { if (err.status === 400 && err instanceof SyntaxError && 'body' in err) { console.error('Bad JSON'); res.status(400).send({ message: 'json body malformed' }); } else { res.status(500).send({ message: 'unknown error' }); } } else { next(err); } }); } initialize(custom_routes, callback) { var deferred = Q.defer(); // --------------------------------- // SETUP EXPRESS // --------------------------------- FullyAPI.prototype.app = express_1.default(); FullyAPI.prototype.app.set('views', path_1.default.join(__dirname, 'views')); // MAP views TO FOLDER STRUCTURE FullyAPI.prototype.app.set('view engine', 'jade'); // USE JADE FOR TEMPLATING FullyAPI.prototype.app.use(body_parser_1.default.json({ limit: config.server_options.max_request_size })); // THIS IS A HIGH DEFAULT LIMIT SINCE TO ALLOW BASE64 ENCODED FILE UPLOAD FullyAPI.prototype.app.use(body_parser_1.default.urlencoded({ extended: true })); // DETERMINE IF THIS IS HTML OR JSON REQUEST FullyAPI.prototype.app.use(express_1.default.static(path_1.default.join(__dirname, 'public'))); // MAP STATIC PAGE CALLS TO public FOLDER FullyAPI.prototype.app.use(cors()); console.log('__dirname', __dirname); console.log('__dirnamepublic', __dirname + 'public'); if (config.server_options.debug_mode != null && (config.server_options.debug_mode === true || config.server_options.debug_mode.toString().toLowerCase() === 'true')) { process.on('warning', (e) => console.warn(e.stack)); // USEFUL IN DEBUGGING process.on('unhandledRejection', (r) => console.log(r)); // USEFUL IN DEBUGGING } console.log('=================================================='); console.log('FullyAPI is Booting'); console.log('=================================================='); settings .init(config.remoteSettingsS3.bucket_name, useRemoteSettings) .then(function (settings_res) { schemaControl.setSettings(settings); // tslint:disable-next-line:no-console console.log('Settings initialized'); utilities = new utilities_1.Utilities(settings); // tslint:disable-next-line:no-console console.log('Utilities initialized'); models = new models_1.Models(settings); return models.init(config.remoteSettingsS3.bucket_name, config.file_locations.models, useRemoteSettings); }) .then(function (endpoints_res) { // tslint:disable-next-line:no-console console.log('Models initialized'); endpoints = new endpoints_1.Endpoints(settings); return endpoints.init(config.remoteSettingsS3.bucket_name, config.file_locations.endoints, config.file_locations.endoints_ext, useRemoteSettings); }) .then(function (model_res) { // tslint:disable-next-line:no-console console.log('Endpoints initialized'); utilities.setEndpointData(endpoints.getEndpointData()); utilities.setModelsData(models.data.models); dataAccess = new dataAccess_1.DataAccess(config, models.data.models, utilities, settings); // tslint:disable-next-line:no-console console.log('DataAccess initialized'); // NOW SET THE DATA ACCESS VAR IN UTILITIES utilities.setDataAccess(dataAccess); serviceRegistration = new serviceRegistration_1.ServiceRegistration(dataAccess, endpoints, models.data.models, utilities); // tslint:disable-next-line:no-console console.log('ServiceRegistration initialized'); accessControl = new accessControl_1.AccessControl(config, utilities, settings, dataAccess); return accessControl.init(config.remoteSettingsS3.bucket_name, config.file_locations.security, useRemoteSettings); }) .then(function (acl_res) { // tslint:disable-next-line:no-console console.log('AccessControl initialized'); mainController = new controller_1.Controller(dataAccess, utilities, accessControl, serviceRegistration, settings, models.data.models, endpoints.data, config); // tslint:disable-next-line:no-console console.log('Controller initialized'); // CREATE ANY NEW DB TABLES BASED ON MODELS return schemaControl.updateSchema(config.db.name, config.db.user, config.db.pass, config.db.host, config.db.port, utilities); }) .then(function (ge_res) { // tslint:disable-next-line:no-console console.log('Schema updated'); // CREATE ANY NEW DB TABLES BASED ON MODELS return schemaControl.createDefaultUser(utilities); }) .then(function (us_Res) { console.log('Default Superuser created'); // CREATE A LOG DIRECTORY IF NEEDED // DO IT SYNCHRONOUSLY WHICH IS ALRIGHT SINCE THIS IS JUST ONCE // DURING STARTUP if (!fs_1.default.existsSync('./logs')) fs_1.default.mkdirSync('./logs'); FullyAPI.prototype.changeErrorLogs(); utilities.setLogs(eventLog, errorLog, sessionLog); console.log('Log files opened'); // SERVER PORT FullyAPI.prototype.app.set('port', config.server_options.server_port); // STARTUP THE SESSION INVALIDATION -- CHECK EVERY X MINUTES const timeoutInMintues = config.server_options.user_session_timeout_min; const invalidSessionTimer = setInterval(function () { FullyAPI.prototype.checkForInvalidSessions(dataAccess, settings); }, config.server_options.user_session_timeout_check_interval * 60000); deferred.resolve(); }) .fail(function (err) { console.log('Initialization Failure'); console.log(err); deferred.reject(); }); deferred.promise.nodeify(callback); return deferred.promise; } sendRes(pipline_results, req, res) { res.status(200).send(pipline_results); } // ================================================ // MAIN REQUEST PIPELINE // ================================================ requestPipeline(req, res, verb, afterPipeline) { const params = req.params; const area = params.area; const controller = params.controller; const serviceCall = !params.serviceCall ? settings.data.index_service_call != null ? settings.data.index_service_call : 'index' : params.serviceCall; let args; if (verb.toLowerCase() === 'get') { args = req.query; } else if (verb.toLowerCase() === 'delete') { // CHECK THE BODY FIRST, IF THERE IS NO BODY OR THE BODY IS EMPTY // CHECK THE QUERY STRING. args = utilities.isNullOrUndefinedOrZeroLength(req.body) ? {} : req.body; const argKeys = Object.keys(args); if (argKeys.length === 0) { args = req.query; } } else { args = req.body; } const version = params.version; let accessLogEvent; if (config.server_options.access_logging === true) { const endpointString = area + '/' + controller + '/' + serviceCall + '/' + version; const ips = req.headers['x-forwarded-for'] || req.connection.remoteAddress; accessLogEvent = { start_timestamp: new Date().toISOString(), endpoint: endpointString, user_agent: req.headers['user-agent'], ips, }; } serviceRegistration .serviceCallExists(serviceCall, area, controller, verb, version) .then(function (sc) { if (sc.authRequired == true) { if (req.headers[settings.data.token_header] != null || (req.headers[settings.data.token_header] == null && req.headers.authorization == null)) { // IF THERE IS fullyAPI STYLE AUTH HEADER OR NEITHER A HEADER TOKEN STYLE AUTH HEADER NOR BASIC/BEARER AUTH HEADER return [sc, accessControl.validateToken(req.headers[settings.data.token_header])]; } else { // OTHERWISE THIS IS BASIC AUTH const authType = req.headers.authorization.split(' ')[0]; if (authType.toLowerCase() === 'basic') { return [sc, accessControl.validateBasicAuth(req.headers.authorization)]; } else { return [ sc, Q.reject(new ErrorObj_1.ErrorObj(403, 'bs0001', __filename, 'requestPipeline', 'bad or unsuported auth type', 'Unauthorized', null)), ]; } } } else { return [sc, { is_valid: false }]; } }) .spread(function (sc, validTokenResponse) { const inner_deferred = Q.defer(); if (validTokenResponse.is_valid === true) { // SEE IF THIS IS HEADER TOKEN STYLE AUTH OR BASIC/BEARER AUTH if (validTokenResponse.hasOwnProperty('session')) { if (config.server_options.access_logging === true) accessLogEvent.session_id = validTokenResponse.session.id; dataAccess .findOneByIDField('fsapi_user_auth', 'user_id', validTokenResponse.session.user_id) .then(function (usr) { inner_deferred.resolve(usr); }) .fail(function (usr_err) { if (sc.authRequired) { const errorObj = new ErrorObj_1.ErrorObj(403, 'bs0002', __filename, 'requestPipeline', 'unauthorized', 'Unauthorized', null); inner_deferred.reject(errorObj); } else { inner_deferred.resolve(null); } }); } else if (validTokenResponse.hasOwnProperty('user')) { inner_deferred.resolve(validTokenResponse.user); } else { if (config.server_options.access_logging === true) accessLogEvent.client_id = validTokenResponse.client_id; dataAccess .findOneByIDField('fsapi_user_auth', 'user_id', validTokenResponse.client_id) .then(function (usr) { inner_deferred.resolve(usr); }) .fail(function (usr_err) { if (sc.authRequired) { const errorObj = new ErrorObj_1.ErrorObj(403, 'bs0003', __filename, 'requestPipeline', 'unauthorized', 'Unauthorized', null); inner_deferred.reject(errorObj); } else { inner_deferred.resolve(null); } }); } } else { inner_deferred.resolve(null); } return [sc, validTokenResponse, inner_deferred.promise]; }) .spread(function (sc, validTokenResponse, userOrNull) { // PUT THE USER OBJECT ON THE REQUEST if (userOrNull !== null) { req.this_user = userOrNull; } if (sc.authRequired) { return [sc, validTokenResponse, accessControl.verifyAccess(req, sc)]; } else { return [sc, validTokenResponse]; } }) .spread(function (sc, validTokenResponse) { return [ sc, validTokenResponse, serviceRegistration.validateArguments(serviceCall, area, controller, verb, version, args), ]; }) .spread(function (sc, validTokenResponse) { return [validTokenResponse, mainController.resolveServiceCall(sc, req)]; }) .spread(function (validTokenResponse, results) { if (validTokenResponse.session != null) { const session = validTokenResponse.session; dataAccess.touchSession(session.id); } // IF ACCESS LOGGING IS ENABLED. ADD THE END TIMESTAMP // AND RESPONSE STATUS NUM TO THE ACCESS LOG EVENT AND // WRITE IT TO THE LOG if (settings.data.access_logging === true) { accessLogEvent.end_timestamp = new Date().toISOString(); accessLogEvent.http_status = 200; const logEntry = JSON.stringify(accessLogEvent) + '\n'; accessLog.write(logEntry); } if (afterPipeline) { afterPipeline(results, req, res); } else { FullyAPI.prototype.sendRes(results, req, res); } }) .fail(function (err) { if (err.http_status == null) { err.http_status = 500; } if (err.message == null || err.message.length === 0) { err.message = 'Something went wrong and we are working to fix it. Please try again later.'; } // IF ACCESS LOGGING IS ENABLED. ADD THE END TIMESTAMP // AND RESPONSE STATUS NUM TO THE ACCESS LOG EVENT AND // WRITE IT TO THE LOG if (settings.data.access_logging === true) { accessLogEvent.end_timestamp = new Date().toISOString(); accessLogEvent.http_status = err.http_status; const logEntry = JSON.stringify(accessLogEvent) + '\n'; accessLog.write(logEntry); } const errorLogEntry = JSON.stringify(err) + '\n'; errorLog.write(errorLogEntry); res.status(err.http_status).send(err); }); } // ----------------------------------- // SWITCH ERROR LOGS AT MIDNIGHT // ----------------------------------- changeErrorLogs() { const today = new Date(); const monthNum = today.getMonth() + 1; const monthString = monthNum < 10 ? '0' + monthNum : monthNum; const dateString = today.getDate() < 10 ? '0' + today.getDate() : today.getDate(); const todayString = monthString + '-' + dateString + '-' + today.getFullYear(); const errorLogPath = './logs/error-' + todayString; const accessLogPath = './logs/access-' + todayString; const sessionLogPath = './logs/session-' + todayString; const eventLogPath = './logs/event-' + todayString; const newErrorLog = fs_1.default.createWriteStream(errorLogPath, { flags: 'a' }); const newEventLog = fs_1.default.createWriteStream(eventLogPath, { flags: 'a' }); let newAccessLog = null; if (settings.data.access_logging === true) newAccessLog = fs_1.default.createWriteStream(accessLogPath, { flags: 'a' }); let newSessionLog = null; if (settings.data.session_logging === true) newSessionLog = fs_1.default.createWriteStream(sessionLogPath, { flags: 'a' }); if (errorLog != null) errorLog.end(); errorLog = newErrorLog; if (eventLog != null) eventLog.end(); eventLog = newEventLog; if (settings.data.access_logging === true) { if (accessLog != null) accessLog.end(); accessLog = newAccessLog; } else { accessLog = null; } if (settings.data.session_logging === true) { if (sessionLog != null) sessionLog.end(); sessionLog = newSessionLog; } else { sessionLog = null; } // DELETE LOGS OLDER THAN today - log_rotation_period const evictionDate = new Date(); evictionDate.setDate(evictionDate.getDate() - config.server_options.log_rotation_period_days); evictionDate.setHours(0, 0, 0, 0); fs_1.default.readdir('./logs/', (err, files) => { if (!err) { for (let fIdx = 0; fIdx < files.length; fIdx++) { const filepath = './logs/' + files[fIdx]; fs_1.default.stat(filepath, (stat_err, stats) => { if (!stat_err) { const createDate = new Date(stats.birthtime); createDate.setHours(0, 0, 0, 0); if (createDate < evictionDate) { fs_1.default.unlink(filepath, (del_err) => { if (del_err) { const errObj = { display_message: 'Problem evicting log files', file: filepath, timestamp: new Date(), results: del_err, }; const logEntry = JSON.stringify(errObj) + '\n'; errorLog.write(logEntry); } }); } } else { const errObj = { display_message: 'Problem evicting log files', file: filepath, timestamp: new Date(), results: stat_err, }; const logEntry = JSON.stringify(errObj) + '\n'; errorLog.write(logEntry); } }); } } else { const errObj = { display_message: 'Problem evicting log files', timestamp: new Date(), results: err, }; const logEntry = JSON.stringify(errObj) + '\n'; errorLog.write(logEntry); } }); const midnightTonight = new Date(); midnightTonight.setDate(midnightTonight.getDate() + 1); midnightTonight.setHours(0, 0, 0, 0); const rightNow = new Date(); const interval = midnightTonight.getTime() - rightNow.getTime(); setTimeout(FullyAPI.prototype.changeErrorLogs, interval); return; } // ---------------------------------------- // CHECK FOR SESSIONS WHICH HAVE TIMED OUT // ---------------------------------------- checkForInvalidSessions(dataAccess, settings, callback) { const deferred = Q.defer(); // THIS RETURNS STALE SESSIONS dataAccess .getDeadSessions(config.server_options.user_session_timeout_min, true) .then(function (deadSessions) { const dsIds = deadSessions[0].ids ? deadSessions[0].ids : []; if (dsIds.length > 0) { dataAccess .deleteSessions(dsIds) .then(function (res) { // IF LOGGING SESSIONS, WRITE OUT THE DEAD SESSIONS TO // THE SESSION LOG if (config.server_options.session_logging === true) { dataAccess .getDeadSessions(config.server_options.user_session_timeout_min) .then(function (deadSessions) { for (let sIdx = 0; sIdx < deadSessions.length; sIdx++) { const dsObj = { session_id: deadSessions[sIdx].id, token: deadSessions[sIdx].token, user_id: deadSessions[sIdx].user_id, started_at: deadSessions[sIdx].started_at, ended_at: new Date(), }; const logEntry = JSON.stringify(dsObj) + '\n'; sessionLog.write(logEntry); } }) .finally(function () { deferred.resolve(); }); } else { deferred.resolve(); } }) .fail(function (err) { const logEntry = JSON.stringify(err) + '\n'; errorLog.write(logEntry); deferred.resolve(); }); } else { deferred.resolve(); } }); deferred.promise.nodeify(callback); return deferred.promise; } } exports.FullyAPI = FullyAPI; // var fullyAPI = new FullyAPI(); // fullyAPI.initialize().then(function () { // fullyAPI.app.post( // '/test/:area/:controller/:serviceCall/:version?', // function (req: any, res: any) { // fullyAPI.requestPipeline(req, res, 'POST'); // } // ); // fullyAPI.configureRoutes(); // fullyAPI.startServer(); // }); //# sourceMappingURL=Server.js.map