fully-api
Version:
API framework for Fully Stacked, LLC REST-ful APIs
624 lines • 30.6 kB
JavaScript
;
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