/* eslint-disable no-console */ /* eslint-disable no-prototype-builtins */ /* eslint-disable no-param-reassign */ /* eslint-disable max-len */ /* eslint-disable consistent-return */ /* eslint-disable import/no-dynamic-require */ /* eslint-disable global-require */ /* Example object * * * { name: "camelCaseName", env: "PROD", version: "1.0.0", query: "PascalCase_With_Underscores_For_NameSpacing", auth: "jwt_string", data: { ... // Specific data to the obj }, results: { name: "camelCaseNameForResults", include: [ { name: "camelCaseName", key: "keyNameToInclude" } ], exclude: [ { key: "keyNameToExclude" } ] } } * * */ import http from 'http' import { v4 } from 'uuid' import { CronJob } from 'cron' import { createPool } from 'mysql2/promise' import { ServerUtils_IsUserAuthorised } from './ServerUtils/IsUserAuthorised/index' import { ServerUtils_SetAuthKeys } from './ServerUtils/SetAuthKeys/index' import { State, HttpResponseObj, PrivateState, } from './types' import { ServerUtils_GetAuthKeys } from './ServerUtils/GetAuthKeys' import prisma from './ServerUtils/Prisma' require('dotenv').config() const mysqlPool = createPool({ host: process.env.DB_HOSTNAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: false, connectionLimit: 5, }) const databasePools = { pool: mysqlPool, prisma, } export const startServer = async () => { await ServerUtils_SetAuthKeys('authKeys.txt') const environment = process.env.ENVIRONMENT console.log('Environment Set To: ', environment) const showLogging = process.env.SHOW_LOGS === 'true' console.log('Showing Logging: ', showLogging) const apiFolder = '../API' const { Endpoints, LoginRequired, ApiKeyAccess } = require(`${apiFolder}`) const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const state: State = {} state.devErrors = [] const privateState: PrivateState = {} const start: number = new Date().valueOf() console.log(`Start: ${start}`) state.body = {} let singleResult = false const httpResponseObj: HttpResponseObj = { complete: false, statusCode: 500, message: 'ServerError', success: false, error: 'unknown', } function log(heading: string, obj: string | State) { if (showLogging) { console.log('=================================================================================') console.log(heading) console.log('=================================================================================') console.log('') console.log(obj) console.log('') console.log('=================================================================================') } } function httpResponse(statusCode = 200, message = 'No message', success = true, error = httpResponseObj.error) { httpResponseObj.statusCode = statusCode httpResponseObj.message = message httpResponseObj.success = success httpResponseObj.complete = true httpResponseObj.error = error if (showLogging) httpResponseObj.devMessages = state.devErrors const end: number = new Date().valueOf() - start const seconds = end / 1000 state.responseTime = seconds // Log out information onto the production servers, for debugging. if (environment === 'prod') { const fileNameArray0 = state.fileNameArray !== undefined ? state.fileNameArray[0] : 'unknown' const fileNameArray1 = state.fileNameArray !== undefined ? state.fileNameArray[1] : 'unknown' console.log(`Time: ${state.responseTime} secs Message: ${message} Success: ${success} Path: ${fileNameArray0}/${fileNameArray1}/${state.body.version} Query: ${state.QueryFunctionName} ${!success ? `Error: ${error}` : ''}`) } } const authenticateUser = () => new Promise((resolve, reject) => { privateState.access = false httpResponseObj.authenticated = false privateState.jwt = {} try { if (state.QueryFunction[state.QueryFunctionName] === undefined) { state.devErrors.push('Your Query is undefined.') reject(httpResponse(501, 'Oops.. Query is undefined.', false, 'Query is private or does not exist')) return } if (!Endpoints[state.QueryFunctionName]) { state.devErrors.push('The endpoint you are trying to access, isn\'t flagged as an endpoint. \nIf you want it to be an endpoint, then add it to the API/index.ts file.') return resolve(httpResponse(404, 'Oops.. Fail...', false, 'Access Denied.')) } if (typeof state.QueryFunction[state.QueryFunctionName] === 'function') { const needsLogin = !!LoginRequired[state.QueryFunctionName] const needsAPIKey = !!ApiKeyAccess[state.QueryFunctionName] if (needsLogin || needsAPIKey) { // Check if Login is required if (needsLogin && state.body.auth !== undefined) { return ServerUtils_GetAuthKeys('authKeys.txt') .then((authKeys: any) => ServerUtils_IsUserAuthorised({ jwt: state.body.auth, authKeys }) .then((isUserAuthorisedResponse: any) => { if (isUserAuthorisedResponse.access) { privateState.access = true privateState.jwt = isUserAuthorisedResponse httpResponseObj.authenticated = true return resolve(privateState.access) } privateState.access = false state.devErrors.push('Your JWT is not authorised. reissue it and try again.') return resolve(httpResponse(401, 'Oops.. Access Denied, please login', false, 'Access Denied. Login Required.')) }).catch(() => reject(httpResponse(401, 'Oops.. Query Failed.', false, 'Query Failed')))) // Check if the API Key is required } else if (needsAPIKey && state.body.apiKey !== undefined) { return ServerUtils_GetAuthKeys('authKeys.txt') .then((authKeys: any) => ServerUtils_IsUserAuthorised({ jwt: state.body.auth, authKeys }) .then((isUserAuthorisedResponse: any) => { if (isUserAuthorisedResponse.access) { privateState.access = true privateState.jwt = isUserAuthorisedResponse httpResponseObj.authenticated = true return resolve(privateState.access) } privateState.access = false state.devErrors.push('Your API Key is not authorised.') return resolve(httpResponse(401, 'Oops.. Access Denied, please login', false, 'Access Denied. Login Required.')) }).catch(() => reject(httpResponse(401, 'Oops.. Query Failed.', false, 'Query Failed')))) } else { let errorMessage = '' if (needsLogin) { state.devErrors.push('Auth is set to required for this endpoint, but there is no auth key') state.devErrors.push("Set `auth: '{{API_AUTH_JWT}}'` in the correct position in request body.") errorMessage += ' No Auth Token in request' } if (needsAPIKey) { state.devErrors.push('API key required is set to required for this endpoint, but there is no API key') state.devErrors.push("Set `apiKey: '{{API_KEY}}'` in the correct position in request body.") errorMessage += ' No API Key in request' } return resolve(httpResponse(401, 'Oops.. Fail...', false, `Access Denied - ${errorMessage}`)) } } // It's a public function, they can do what ever the hell they want.. Call away!.. privateState.access = true resolve(privateState.access) return } state.devErrors.push('The endpoint does not have the type of "function" so therefore it cannot be triggered. Variables are not endpoints') reject(httpResponse(500, 'Oops.. Query is not a function.', false, 'Query is private or does not exist')) return } catch (error) { log('authenticateUser Error', error) state.devErrors.push('There was an error in authenticating the user, try reissuing the JWT, and making sure the secrets Cron Job is working') return reject(httpResponse(511, 'Catch.. Oops.. Query Version is protected or does not exist.', false, 'Query is private or does not exist')) } }) const callFunctionReturnResponse = () => new Promise((resolve) => { if (privateState.access) { // log('body', state.body) state.body.data.authJWT = privateState.jwt state.body.data.env = environment return state.QueryFunction[state.QueryFunctionName](state.body.data, databasePools).then((apiResponse) => { let includeLength = 0 let excludeLength = 0 if (state.body.results !== undefined) { if (state.body.results.include !== undefined) includeLength = state.body.results.include.length if (state.body.results.exclude !== undefined) excludeLength = state.body.results.exclude.length // include results if (includeLength === 1) { if (apiResponse[state.body.results.include[0].key] !== undefined) { singleResult = true apiResponse = apiResponse[state.body.results.include[0].key] httpResponseObj.response = apiResponse[state.body.results.include[0].key] // log('Only 1 apiResponse:::: ', apiResponse) } } else if (includeLength > 1) { const newIncludeApiResponse = {} state.body.results.include.forEach((includeObj: { key: string | number; name: string | number }) => { if (apiResponse.hasOwnProperty([includeObj.key])) { newIncludeApiResponse[includeObj.name] = apiResponse[includeObj.key] } }) apiResponse = newIncludeApiResponse // log('Multiple apiResponses::::', apiResponse) } if (excludeLength > 0) { // exclude results state.body.results.exclude.forEach((excludeObj: { key: string | number }) => { if (apiResponse.hasOwnProperty([excludeObj.key])) { delete apiResponse[excludeObj.key] } }) // log('Excluded apiResponse::: ', apiResponse) } // Put the results into the state. And name it as required. if (state.body.results.name !== undefined) { state[state.body.results.name] = apiResponse httpResponseObj[state.body.results.name] = apiResponse if (singleResult) { httpResponseObj.response = apiResponse } } } else { state.results = apiResponse httpResponseObj.results = apiResponse } resolve(httpResponse(200, 'complete', true)) }).catch((e) => { log('ERROR:::: Internal Function Error', e) state.devErrors.push('ERROR:::: Internal Function Error') resolve(httpResponse(500, 'Internal Function Error', false, 'Internal Function Error')) }) } state.devErrors.push('Your access is denied, Reissue your JWT if the function required authentication') resolve(httpResponse(401, 'Access Denied.', false)) throw new Error('No Access') }) function completeCall() { response.writeHead(httpResponseObj.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'x-jwt, content-type', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload', }) let responseState = httpResponseObj.response if (!singleResult) { responseState = httpResponseObj if (httpResponseObj.success) { delete responseState.error } } log('FINAL RESPONSE', responseState) response.end(JSON.stringify(responseState)) } function setFileName() { log('version', state.body) const version = state.body.version.replace(/\./g, '_') state.QueryFunctionName = `${state.body.query}_${version}` state.fileNameArray = state.body.query.split('_') log('state', state) } try { if (request.method === 'POST') { const body: Buffer[] = [] request.on('data', (chunk) => { body.push(chunk) }).on('end', () => { try { const data = Buffer.concat(body).toString() const parsedBody = JSON.parse(data) // Check if URL is coming from a webhook if (request.url?.includes('/Webhook_')) { const url = request.url?.slice(1).split('_') state.body.version = `${url[2]}.${url[3]}.${url[4]}` state.body.query = `${url[0]}_${url[1]}` state.body.data = {} state.body.data.parsed = parsedBody state.body.data.headers = request.headers state.body.data.raw = data state.body.data.correlationId = v4() } else { state.body = parsedBody if (!state.body.data.correlationId) state.body.data.correlationId = v4() } } catch (error) { console.log('ERR 1', error) state.devErrors.push('Cannot Parse the body.data obj into JSON. Check the object structure in the data obj: { \'data\': {} }') httpResponse(500, 'Fail.. Query does not exist. Check the name and version', false, 'Query does not exist') } try { // Import the Function to be executed. setFileName() const folderPath = `${apiFolder}/${state.fileNameArray[0]}/${state.fileNameArray[1]}/${state.body.version}` log('folderPath', folderPath) state.QueryFunction = require(folderPath) // TODO:: Foreach object here, run the following.. return authenticateUser().then(() => callFunctionReturnResponse()).then(() => { httpResponse() return completeCall() }).catch((e) => { log('ERR 3', e) state.devErrors.push('An error occurred when trying to authenticate and then call the function') httpResponse(403, 'Oops.. Query is protected or does not exist.', false, 'Query is private or does not exist') return completeCall() }) } catch (error) { log('ERR 1', error) state.devErrors.push('Check the query name in the body JSON is correct. { \'query\': \'Function_Name\' }') state.devErrors.push('CORE ONLY:: There are no other functions') httpResponse(500, 'Fail.. Query does not exist. Check the name and version', false, 'Query does not exist') return completeCall() } }) } else if (request.method === 'OPTIONS') { httpResponse(200, 'Granted', true, 'Access Granted') return completeCall() } else { state.devErrors.push('You can NOT use a GET / PUT / DELETE method. Change it to POST') httpResponse(405, 'Access Denied. POST Only.', false, 'Access Denied. POST Only') return completeCall() } } catch (error) { console.log('ERR 2', error) state.devErrors.push('Something major is not working. Ask Waz') httpResponse(500, 'Oops, Internal Server Error.', false, 'Internal Server Error') return completeCall() } }) // This starts the cron job ready to be run every 59 mins console.log('Started Cron Job') const onTick = () => { console.log('Auth Keys Updating @', new Date()) return ServerUtils_SetAuthKeys('authKeys.txt') } const job = new CronJob('0 */30 * * * *', onTick) job.start() if (environment === 'localhost') { const port = 3030 const host = 'localhost' server.listen(port, host) console.log(`Listening at http://${host}:${port}`) } else { const host = '127.0.0.1' const port = 3000 server.listen(port, host) console.log(`Listening at ${host}:${port}`) } }