From 43bcc7c0f320874c193472e1eaa1c474269f2163 Mon Sep 17 00:00:00 2001 From: Josepablo C Date: Mon, 21 Jul 2025 12:07:46 -0600 Subject: [PATCH] fix(loads): Normalize dates on post and patch (remove time from dates) --- v1/src/apps/private/loads/services.js | 102 ++++- v1/src/config/apiConfig.json | 4 +- v1/src/lib/Handlers/Events/Loads/index.js | 7 + v1/src/lib/Misc.js | 15 +- v2/Dockerfile | 17 - v2/dotenv | 1 - v2/package.json | 56 --- v2/scripts/ci_functions.sh | 69 ---- v2/server/README.md | 5 - v2/server/config/apiConfig.json | 55 --- v2/server/docs/assets/APIDesign.drawio | 118 ------ v2/server/docs/assets/APIDesign.png | Bin 61993 -> 0 bytes .../src/Apps/Account/Controller/hooks.js | 169 -------- .../src/Apps/Account/Controller/index.js | 13 - v2/server/src/Apps/Account/Domain/index.js | 217 ---------- .../src/Apps/Account/Interfaces/index.js | 47 --- .../Account/Repository/Objection/index.js | 97 ----- .../src/Apps/Account/Repository/index.js | 5 - .../src/Apps/Company/Controller/hooks.js | 38 -- .../src/Apps/Company/Controller/index.js | 13 - v2/server/src/Apps/Company/Domain/index.js | 34 -- .../src/Apps/Company/Interfaces/index.js | 27 -- .../Company/Repository/Objection/index.js | 3 - .../src/Apps/Company/Repository/index.js | 5 - v2/server/src/Apps/Lib/index.js | 116 ------ .../Controller/graphql/resolvers.js | 53 --- .../Controller/graphql/schema.graphql | 146 ------- .../Controller/graphql/schema.js | 8 - .../Apps/PrivateResources/Controller/hooks.js | 50 --- .../Apps/PrivateResources/Controller/index.js | 13 - .../src/Apps/PrivateResources/Domain/index.js | 389 ------------------ .../Apps/PrivateResources/Interfaces/index.js | 27 -- .../Repository/Objection/index.js | 68 --- .../Apps/PrivateResources/Repository/index.js | 5 - v2/server/src/Controller/index.js | 45 -- v2/server/src/Controller/middlewares.js | 60 --- v2/server/src/Controller/resources.js | 12 - v2/server/src/Shared/ErrorResponse.js | 12 - .../Models/Objection/companies.model.js | 24 -- .../Objection/company_categories.model.js | 19 - .../Objection/company_truck_types.model.js | 19 - .../src/Shared/Models/Objection/index.js | 55 --- .../Objection/load_attachments.model.js | 32 -- .../Models/Objection/load_categories.model.js | 19 - .../Shared/Models/Objection/loads.model.js | 60 --- .../Objection/location_categories.model.js | 19 - .../Objection/location_truck_types.model.js | 19 - .../Models/Objection/locations.model.js | 25 -- .../Objection/metadata_categories.model.js | 18 - .../Models/Objection/metadata_cities.model.js | 21 - .../Objection/metadata_products.model.js | 18 - .../Objection/metadata_truck_types.model.js | 18 - .../Models/Objection/user_locations.model.js | 36 -- .../Models/Objection/user_sessions.model.js | 20 - .../Shared/Models/Objection/users.model.js | 36 -- .../Objection/vechicle_publications.model.js | 32 -- .../Objection/vehicle_categories.model.js | 19 - .../Shared/Models/Objection/vehicles.model.js | 34 -- v2/server/src/Shared/Resources/index.js | 38 -- v2/server/src/Shared/ShaUtils.js | 12 - v2/server/src/SysS/Connections/index.js | 69 ---- v2/server/src/SysS/Controller/index.js | 140 ------- v2/server/src/SysS/Controller/middlewares.js | 87 ---- .../EmailEvents/SendGrid.handler.js | 84 ---- .../EmailEvents/StandAlone.handler.js | 22 - .../SysS/EventManager/EmailEvents/index.js | 36 -- v2/server/src/SysS/EventManager/index.js | 71 ---- v2/server/src/SysS/EventManager/resources.js | 7 - v2/server/src/SysS/Lib/index.js | 33 -- v2/server/src/SysS/index.js | 78 ---- v2/server/src/index.js | 32 -- v2/server/test/index.js | 18 - 72 files changed, 118 insertions(+), 3273 deletions(-) delete mode 100644 v2/Dockerfile delete mode 100644 v2/dotenv delete mode 100644 v2/package.json delete mode 100644 v2/scripts/ci_functions.sh delete mode 100644 v2/server/README.md delete mode 100644 v2/server/config/apiConfig.json delete mode 100644 v2/server/docs/assets/APIDesign.drawio delete mode 100644 v2/server/docs/assets/APIDesign.png delete mode 100644 v2/server/src/Apps/Account/Controller/hooks.js delete mode 100644 v2/server/src/Apps/Account/Controller/index.js delete mode 100644 v2/server/src/Apps/Account/Domain/index.js delete mode 100644 v2/server/src/Apps/Account/Interfaces/index.js delete mode 100644 v2/server/src/Apps/Account/Repository/Objection/index.js delete mode 100644 v2/server/src/Apps/Account/Repository/index.js delete mode 100644 v2/server/src/Apps/Company/Controller/hooks.js delete mode 100644 v2/server/src/Apps/Company/Controller/index.js delete mode 100644 v2/server/src/Apps/Company/Domain/index.js delete mode 100644 v2/server/src/Apps/Company/Interfaces/index.js delete mode 100644 v2/server/src/Apps/Company/Repository/Objection/index.js delete mode 100644 v2/server/src/Apps/Company/Repository/index.js delete mode 100644 v2/server/src/Apps/Lib/index.js delete mode 100644 v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js delete mode 100644 v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql delete mode 100644 v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js delete mode 100644 v2/server/src/Apps/PrivateResources/Controller/hooks.js delete mode 100644 v2/server/src/Apps/PrivateResources/Controller/index.js delete mode 100644 v2/server/src/Apps/PrivateResources/Domain/index.js delete mode 100644 v2/server/src/Apps/PrivateResources/Interfaces/index.js delete mode 100644 v2/server/src/Apps/PrivateResources/Repository/Objection/index.js delete mode 100644 v2/server/src/Apps/PrivateResources/Repository/index.js delete mode 100644 v2/server/src/Controller/index.js delete mode 100644 v2/server/src/Controller/middlewares.js delete mode 100644 v2/server/src/Controller/resources.js delete mode 100644 v2/server/src/Shared/ErrorResponse.js delete mode 100644 v2/server/src/Shared/Models/Objection/companies.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/company_categories.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/company_truck_types.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/index.js delete mode 100644 v2/server/src/Shared/Models/Objection/load_attachments.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/load_categories.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/loads.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/location_categories.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/location_truck_types.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/locations.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/metadata_categories.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/metadata_cities.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/metadata_products.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/metadata_truck_types.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/user_locations.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/user_sessions.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/users.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/vechicle_publications.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/vehicle_categories.model.js delete mode 100644 v2/server/src/Shared/Models/Objection/vehicles.model.js delete mode 100644 v2/server/src/Shared/Resources/index.js delete mode 100644 v2/server/src/Shared/ShaUtils.js delete mode 100644 v2/server/src/SysS/Connections/index.js delete mode 100644 v2/server/src/SysS/Controller/index.js delete mode 100644 v2/server/src/SysS/Controller/middlewares.js delete mode 100644 v2/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js delete mode 100644 v2/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js delete mode 100644 v2/server/src/SysS/EventManager/EmailEvents/index.js delete mode 100644 v2/server/src/SysS/EventManager/index.js delete mode 100644 v2/server/src/SysS/EventManager/resources.js delete mode 100644 v2/server/src/SysS/Lib/index.js delete mode 100644 v2/server/src/SysS/index.js delete mode 100644 v2/server/src/index.js delete mode 100644 v2/server/test/index.js diff --git a/v1/src/apps/private/loads/services.js b/v1/src/apps/private/loads/services.js index d52d2a7..9c63f03 100644 --- a/v1/src/apps/private/loads/services.js +++ b/v1/src/apps/private/loads/services.js @@ -1,6 +1,6 @@ "use strict"; const { getModel } = require( '../../../lib/Models' ); -const { getPagination, genKey } = require( '../../../lib/Misc.js' ); +const { getPagination, genKey, normalizeDate } = require( '../../../lib/Misc.js' ); const { GenericHandler } = require( '../../../lib/Handlers/Generic.handler.js' ); const { onPatchEvent } = require('../../../lib/Handlers/Loads.handler'); const Model = getModel('loads'); @@ -275,6 +275,25 @@ const getById = async(req, res) => { } }; +function normalizeDatesFromData( data ){ + let fields2normalize = [ + "contract_start_date", + "contract_end_date", + "est_loading_date", + "est_unloading_date", + "published_date", + "loaded_date", + "transit_date", + "delivered_date", + ]; + for( const item of fields2normalize ){ + if( Object.hasOwn( data, item ) ){ + data[ item ] = normalizeDate( data[ item ] ) + } + } + return data; +} + function getDataToModify( data ){ /** * Take the only fields from model that @@ -329,7 +348,7 @@ function getDataToModify( data ){ throw "nothing to change"; } - return filtered_data; + return normalizeDatesFromData( filtered_data ); } const patchLoad = async(req, res) => { try{ @@ -357,23 +376,92 @@ const patchLoad = async(req, res) => { } }; +function getDataToCreate( data ){ + /** + * Take the only fields from model that + * should be modifiable by the client. + * The rest are populated on demand by the event handlers. + */ + let data_fields = { + carrier: null, + vehicle: null, + driver: null, + bidder: null, + alert_list: null, + origin_warehouse: null, + destination_warehouse: null, + origin: null, + origin_geo: null, + destination: null, + destination_geo: null, + categories: null, + product: null, + truck_type: null, + tyre_type: null, + weight: null, + estimated_cost: null, + distance: null, + actual_cost: null, + status: null, + load_status: null, + contract_start_date: null, + contract_end_date: null, + est_loading_date: null, + est_unloading_date: null, + published_date: null, + loaded_date: null, + transit_date: null, + delivered_date: null, + notes: null, + }; + + let filtered_data = {}; + + if( Object.keys( data_fields ).length === 0 ){ + throw "no data to add"; + } + + for ( const [key, value] of Object.entries( data_fields ) ) { + if( Object.hasOwn( data, key ) ){ + filtered_data[ key ] = data[ key ]; + } + } + + if( Object.keys( filtered_data ).length === 0 ){ + throw "no data to add"; + } + + return normalizeDatesFromData( filtered_data ); +} const postLoad = async(req, res) => { try{ const companyId = req.context.companyId; const userId = req.context.userId; - const user_name = req.context.user.first_name; + let user_name; + + if( req.context.user.last_name ){ + user_name = `${req.context.user.first_name} ${req.context.user.last_name}`; + }else{ + user_name = req.context.user.first_name; + } + const permissions = req.context.permissions; - const data = req.body; - if( !data ){ + + if( !req.body ){ throw "Load data not sent"; } + if(permissions !== "role_shipper" ){ throw "You can't create loads"; } + + const data = getDataToCreate( req.body ); + data.company = companyId; data.posted_by = userId; - data.name = user_name; - const load = new Model( data );/// ToDo, check data content and normalize Dates!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + data.posted_by_name = user_name; + + const load = new Model( data ); await load.save(); const id = "" + load._id; diff --git a/v1/src/config/apiConfig.json b/v1/src/config/apiConfig.json index f0b1d28..1e27560 100644 --- a/v1/src/config/apiConfig.json +++ b/v1/src/config/apiConfig.json @@ -16,9 +16,9 @@ } }, "version" : { - "version" : "1.5.1", + "version" : "1.5.2", "name": "ETA Beta", - "date":"06/2025" + "date":"21/07/2025" }, "S3" : { "bucket": "etaviaporte", diff --git a/v1/src/lib/Handlers/Events/Loads/index.js b/v1/src/lib/Handlers/Events/Loads/index.js index 455481f..5b23b20 100644 --- a/v1/src/lib/Handlers/Events/Loads/index.js +++ b/v1/src/lib/Handlers/Events/Loads/index.js @@ -12,11 +12,18 @@ const proposalsModel = getModel('proposals'); */ async function onDelivered( userId, elementId ){ const load = await loadsModel.findById( elementId ); + + if( !load ){ + /// Nothing to do, invalid load + return; + } + const proposal_list = await proposalsModel.find({ load: elementId, is_accepted: true, is_completed: false }); + const vehicle_list = await vehiclesModel.find({ active_load: elementId }); diff --git a/v1/src/lib/Misc.js b/v1/src/lib/Misc.js index a4c5633..dd2f724 100644 --- a/v1/src/lib/Misc.js +++ b/v1/src/lib/Misc.js @@ -39,6 +39,19 @@ function genKey( len = 6, key="" ){ return otp_str.slice(0,len); } +/** + * Takes a datetime input and produces a date only output. + * @param {*} date + */ +function normalizeDate( date ){ + let in_date = new Date( date ); + let year = in_date.getFullYear(); + let monthIndex = in_date.getMonth(); + let day = in_date.getDate(); + // new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds); + return new Date( year, monthIndex, day, 0, 0, 0, 0 ); +} + function getPagination( query ){ let limit = { page : 0, @@ -101,4 +114,4 @@ async function downloadFile( bucket, key, obj_id ){ return s3resp; } -module.exports = { genKey , toSha256, getPagination, getPage, queryPage, downloadFile}; \ No newline at end of file +module.exports = { genKey , toSha256, normalizeDate, getPagination, getPage, queryPage, downloadFile}; \ No newline at end of file diff --git a/v2/Dockerfile b/v2/Dockerfile deleted file mode 100644 index 5932e33..0000000 --- a/v2/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use an official Python runtime as a parent image -FROM node:18-alpine - -# Set the working directory to /app -WORKDIR /app - -# Copy the current directory contents into the container at /app -COPY server /app/server -COPY package.json /app -COPY dotenv /app/.env - -RUN apk add bash bash-completion vim -RUN npm install --include=dev - -EXPOSE 3000 - -ENTRYPOINT npm start diff --git a/v2/dotenv b/v2/dotenv deleted file mode 100644 index 200a4c0..0000000 --- a/v2/dotenv +++ /dev/null @@ -1 +0,0 @@ -SERVER_PORT=3000 diff --git a/v2/package.json b/v2/package.json deleted file mode 100644 index e4b9c54..0000000 --- a/v2/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "etaapi", - "version": "1.0.0", - "description": "ETA API", - "main": "index.js", - "scripts": { - "start": "nodemon server/src/", - "dev": "nodemon server/src/", - "test": "mocha test/lib/handlers/proposals" - }, - "repository": { - "type": "git", - "url": "git+https://gitlab.com/jcruzbaasworkspace/enruta/etaapi.git" - }, - "author": "Josepablo C.", - "license": "ISC", - "bugs": { - "url": "https://gitlab.com/jcruzbaasworkspace/enruta/etaapi/issues" - }, - "homepage": "https://gitlab.com/jcruzbaasworkspace/enruta/etaapi#readme", - "dependencies": { - "@aws-sdk/client-s3": "^3.427.0", - "audit": "^0.0.6", - "axios": "^1.5.1", - "body-parser": "^1.20.2", - "compression": "^1.7.4", - "cors": "^2.8.5", - "crypto-js": "^4.1.1", - "dotenv": "^16.3.1", - "express": "^4.19.2", - "express-fileupload": "^1.4.1", - "express-jwt": "^8.4.1", - "form-data": "^4.0.0", - "graphql": "^16.9.0", - "graphql-http": "^1.22.1", - "graphql-scalars": "^1.23.0", - "helmet": "^7.0.0", - "jsonschema": "^1.4.1", - "jsonwebtoken": "^9.0.2", - "knex": "^2.5.1", - "mongodb-core": "^3.2.7", - "mongoose": "^7.5.4", - "morgan": "^1.10.0", - "mysql": "^2.18.1", - "nodemailer": "^6.9.5", - "nodemailer-sendgrid": "^1.0.3", - "nodemon": "^3.0.1", - "objection": "^3.1.4", - "uuid": "^9.0.1" - }, - "devDependencies": { - "chai": "^5.1.0", - "mocha": "^10.3.0", - "sinon": "^17.0.1" - } -} diff --git a/v2/scripts/ci_functions.sh b/v2/scripts/ci_functions.sh deleted file mode 100644 index 8eb5b33..0000000 --- a/v2/scripts/ci_functions.sh +++ /dev/null @@ -1,69 +0,0 @@ -#! /bin/bash - -REGISTRY_USER="jcruzbaas@gmail.com" -REGISTRY_NAME="registry.gitlab.com/jcruzbaasworkspace/enruta" -REGISTRY_SERVER="registry.gitlab.com" -CONTAINER_VERSION="2" - -# Requirements -# Docker: Node v18-alpine -function build_docker(){ - #Global ENV VAR: REGISTRY_NAME - #Global ENV VAR: CONTAINER_NAME - if [[ $# -lt 1 ]]; then - echo $0 "[conatiner name]" - return -1 - fi - CONTAINER_NAME=$1 - - set -x - docker rmi -f "$REGISTRY_NAME/$CONTAINER_NAME" - docker buildx build --no-cache -t $REGISTRY_NAME/$CONTAINER_NAME ./ - set +x -} - -function upload_image(){ - #Global ENV VAR: REGISTRY_NAME - #Global ENV VAR: GITLAB_CICD_REGISTRY_TOKEN - #Global ENV VAR: CONTAINER_NAME - docker login $REGISTRY_SERVER -u "$REGISTRY_USER" -p "$GITLAB_CICD_REGISTRY_TOKEN" - set -x - docker push "$REGISTRY_NAME/$CONTAINER_NAME":$CONTAINER_VERSION - set +x -} - -function deploy_uservice(){ - # CONTAINER NAME - # PUBLIC PORT - # PRIVATE PORT - # ECR REPO - if [[ $# -lt 4 ]]; then - echo "$0 [container_name] [public port] [private port] [ecr_repo]" - return -1 - fi - container_name=$1 - public_port=$2 - private_port=$3 - ecr_repo=$4 - - docker stop $container_name && docker rm $container_name - docker run -p"$public_port:$private_port" -d --restart unless-stopped --name $container_name $ecr_repo/$container_name:$CONTAINER_VERSION -} - -function deploy_local(){ - # docker login --username $AWS_ECR_USER --password $DOCKER_PWD $REGISTRY_NAME - deploy_uservice $CONTAINER_NAME $PUBLIC_PORT $PRIVATE_PORT $REGISTRY_NAME -} - -function deploy(){ - # Global ENV VAR: GITLAB_CICD_REGISTRY_TOKEN - # Global Env Var: REGISTRY_NAME - # Global Env Var: REGISTRY_USER - # Global Env Var: CONTAINER_NAME - # Global Env Var: PUBLIC_PORT - # Global Env Var: PRIVATE_PORT - # Global Env Var: SYSTEM_HOSTNAME - set -x - ssh -i ~/.ssh/gitlab_runner gitlab-runner@$SYSTEM_HOSTNAME "docker login $REGISTRY_SERVER --username $REGISTRY_USER --password $GITLAB_CICD_REGISTRY_TOKEN $REGISTRY_NAME && ./deploy_uservice.sh $CONTAINER_NAME $PUBLIC_PORT $PRIVATE_PORT $REGISTRY_NAME && exit" - set +x -} diff --git a/v2/server/README.md b/v2/server/README.md deleted file mode 100644 index 3af4002..0000000 --- a/v2/server/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# REST API Framework - -## Architecture Design - -![alt text](docs/assets/APIDesign.png) \ No newline at end of file diff --git a/v2/server/config/apiConfig.json b/v2/server/config/apiConfig.json deleted file mode 100644 index 19544da..0000000 --- a/v2/server/config/apiConfig.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "authentication": { - "pwdSecret":"Nx2g_IWo2Zt_LS$+", - "jwtSecret":"9o3BBz0EsrwXXiwEJ/SFuywZoN8=", - "jwtTimeout":24, - "jwtRenewalTimeout":720, - "tokenSecret":"9Z'jMt|(h_f(&/S+zv.K", - "jwtOptions": { - "header": { - "typ": "access" - }, - "audience": "https://www.etaviaporte.com", - "issuer": "etaviaporte", - "algorithm": "HS256", - "expiresIn": "1d" - } - }, - "version" : { - "version" : "2.0.0", - "name": "ETA Beta", - "date":"08/2024" - }, - "S3" : { - "accessKeyId": "AKIAXTQEUF6MLCHTUIKW", - "secretAccessKey": "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", - "bucket": "enruta", - "load_attachments_key":"loadattachments", - "news_key":"news", - "region": "us-west-1" - }, - "sendgrid" : { - "HOST": "smtp.sendgrid.net", - "PORT": "465", - "username": "apikey", - "API_KEY": "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", - "FROM": "noreply@etaviaporte.com" - }, - "email_standalone" : { - "host": "smtp.hostinger.com", - "port": "465", - "secure": true, - "auth": { - "user": "noreply@etaviaporte.com", - "pass": "-)WJt[oP~P$`76Q4" - } - }, - "mongodb": "mongodb+srv://enruta_admin:NeptFx4RUZG8OsfA@enruta.vwofshy.mongodb.net/enrutaviaporte?retryWrites=true&w=majority", - "sql":{ - "host":"srv765.hstgr.io", - "port":3306, - "user":"u947463964_sysadmin", - "password":"3^K/47^h5pP", - "database":"u947463964_etaviaporte" - } -} diff --git a/v2/server/docs/assets/APIDesign.drawio b/v2/server/docs/assets/APIDesign.drawio deleted file mode 100644 index ab5fb4f..0000000 --- a/v2/server/docs/assets/APIDesign.drawio +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/v2/server/docs/assets/APIDesign.png b/v2/server/docs/assets/APIDesign.png deleted file mode 100644 index de582c84e642872ecb33347627e9a84bc9ea22fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61993 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≰hr;&a}Es!-TmNmNm!tIkWPq7*Ri8qC7f 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 G=)V9v8ojmv diff --git a/v2/server/src/Apps/Account/Controller/hooks.js b/v2/server/src/Apps/Account/Controller/hooks.js deleted file mode 100644 index 9244c36..0000000 --- a/v2/server/src/Apps/Account/Controller/hooks.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict'; -const Application = require('../Domain'); - -function dummy_middleware( req, res, next ){ - return res.status(500).send({ error:"Not implemented yet" }); -} - -/// Block access to the next list of endpoints if JWT is not valid -async function access_control_with_jwt( req, res, next){ - if( ! req.JWT?.isValid ){ - return res.status(401).send({error:"Unauthorized",code:401}); - } - return next(); -} - -async function authorize(req, res, next) { - try{ - const email = req.body.email; - const password = req.body.password; - const data = await Application.authorize_credentials( email , password ); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function refresh_session(req, res, next) { - try{ - const session_token = req.params.session_token; - const data = await Application.authorize_token( session_token ); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function check_account(req, res, next){ - try{ - const email = req.params.email; - const data = await Application.check_account( email ); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function post_signup(req, res, next) { - try{ - const email = req.body.email; - const password = req.body.password; - const data = await Application.genOTPChecksum( email, password, "signup" ); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function patch_signup(req, res, next) { - try{ - const email = req.body.email; - const password = req.body.password; - const otp = req.body.otp; - const checksum = req.body.checksum; - const data = await Application.signup( email , password, otp, checksum); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function post_recover(req, res, next){ - try{ - const email = req.body.email; - const password = req.body.password; - const data = await Application.genOTPChecksum( email, password, "recover" ); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -async function patch_recover(req, res, next){ - try{ - const email = req.body.email; - const password = req.body.password; - const otp = req.body.otp; - const checksum = req.body.checksum; - const data = await Application.recover( email , password, otp, checksum); - if( data.error ){ - const error = data.error; - return res.status( error.code ).send( { error : error.msg } ); - } - return res.send( data ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send( { error } ); - } -} - -function AppInit(){ - Application.init(); -} - -module.exports = { - AppInit, - hooks : { - before: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [], - del : [], - get : [], - patch : [], - post : [], - put : [] - }, - - after: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [ - { '/register': dummy_middleware } - ], - del : [], - get : [ - { '/check-account/:email' : check_account }, - { '/authorize/:session_token': refresh_session } - ], - patch : [ - { '/signup' : patch_signup }, - { '/recover' : patch_recover } - ], - post : [ - { '/authorize' : authorize }, - { '/signup' : post_signup }, - { '/recover' : post_recover } - ], - put : [] - }, - } -}; diff --git a/v2/server/src/Apps/Account/Controller/index.js b/v2/server/src/Apps/Account/Controller/index.js deleted file mode 100644 index 7c7f554..0000000 --- a/v2/server/src/Apps/Account/Controller/index.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const { hooks, AppInit } = require( './hooks' ); -const GenericController = require('../../Lib'); - -class Account extends GenericController { - init(){ - super.init(); - AppInit(); - } -} - -module.exports = new Account( hooks ); diff --git a/v2/server/src/Apps/Account/Domain/index.js b/v2/server/src/Apps/Account/Domain/index.js deleted file mode 100644 index f8e9750..0000000 --- a/v2/server/src/Apps/Account/Domain/index.js +++ /dev/null @@ -1,217 +0,0 @@ -'use strict'; - -const Repository = require('../Repository'); -const jsonwebtoken = require('jsonwebtoken'); -const { ModuleName, - toSha256, - publishEvent, - registerEvent, - jwtRenewalTimeout, - jwtTimeout, - jwtOptions, - jwtSecret, - tokenSecret, - pwdSecret, - genErrorResponse } = require('../Interfaces'); - - -class Account { - constructor(){ - this.Events = { - /** Event_Id : callback */ - }; - } - - init(){ - /// Setup application events - const Events = this.Events; - for ( const [event, callback] of Object.entries( Events ) ) { - const event_id = ModuleName + event - Interfaces.registerEvent( event_id , callback ); - } - } - - genOTP( email ){ - const len = 5; - const shacode = toSha256( email + new Date() + tokenSecret ); - const otp_hex = shacode.slice(0 , len ); - const otp_dec = Number.parseInt( otp_hex , 16 ); - return ""+otp_dec; - } - - genSafePassword( password ){ - return toSha256( password + pwdSecret ); - } - - async check_account( email ){ - const projection = ["id","email","password","company_id"]; - const user = await Repository.getByEmail( email , projection ); - const retVal = { - has_account:false, - isVerified:false, - has_password:false - }; - - if( !user ){ - retVal.has_account = false; - retVal.isVerified = false; - retVal.has_password = false; - }else{ - retVal.has_account = true; - retVal.isVerified = user.isVerified; - retVal.has_password = ( !user.password )? false : true; - } - - return retVal; - } - - async genOTPChecksum( email, password, reason="signup" ){ - let event_id = reason; - - if( reason === "signup" ){ - event_id = "getchecksum:signup"; - - const it_exists = await Repository.getByEmail( email ); - if( it_exists ){ - return genErrorResponse( "User already registered!" ); - } - - }else if( reason === "recover" ){ - event_id = "getchecksum:recover"; - }else{ - return genErrorResponse( "OPT Event type not defined" , 500 ); - } - - const otp = this.genOTP( email ); - - const content = { OTP : otp, user_name : email, email }; - - const checksum_entry = { - email, - password, - otp - }; - - const checksum = toSha256( JSON.stringify(checksum_entry)).slice(0, 32); - - publishEvent( event_id , content ); - - return { checksum }; - } - - async signup( email, password, otp, checksum ){ - const it_exists = await Repository.getByEmail( email ); - - if( it_exists ){ - return genErrorResponse( "User already registered!" ); - } - - const checksum_entry = {email, password, otp}; - const recomputed_checksum = toSha256( JSON.stringify(checksum_entry)).slice(0, 32); - - if( recomputed_checksum != checksum ){ - return genErrorResponse( "Wrong OTP" ); - } - - await Repository.createOne( email, this.genSafePassword( password ) ); - - const content = { user_name : email, email }; - - publishEvent( "signupconfirmed" , content ); - - return await this.authorize_credentials( email, password ); - } - - async recover( email, password, otp, checksum ){ - const user = await Repository.getByEmail( email ); - - if( !user ){ - return genErrorResponse( "Email is not registered!" ); - } - - const checksum_entry = {email, password, otp}; - const recomputed_checksum = toSha256( JSON.stringify(checksum_entry)).slice(0, 32); - - if( recomputed_checksum != checksum ){ - return genErrorResponse( "Wrong OTP" ); - } - - await Repository.updatePassword( user.id, this.genSafePassword( password ) ); - - return await this.authorize_credentials( email, password ); - } - - async authorize_credentials( email, password ){ - const user = await Repository.findByEmailPassword( email, this.genSafePassword( password ) ); - - if( !user ){ - return genErrorResponse( "Not able to log in", 401 ); - } - const current_date = new Date(); - const iat = Math.floor( (current_date.getTime())/1000 ); - const renewal_exp = ( iat + 3600*jwtRenewalTimeout ) * 1000; - /** - * Renew session token on every login event. - * Previous session token is lost - */ - const session_token = toSha256( `${new Date()}` ); - const session_token_exp = new Date( renewal_exp ); - - await Repository.addSessionToken( user.id, session_token, session_token_exp ); - - const payload = { - iat: iat, - exp: iat + jwtTimeout * 3600, - aud: jwtOptions.audience, - iss: jwtOptions.audience, - sub: user.id, - }; - const jwt = jsonwebtoken.sign( payload , jwtSecret ); - return { - accessToken : jwt, - payload : payload, - session_token, - session_token_exp, - user : user - }; - } - - async authorize_token( token ){ - const user = await Repository.findBySessionToken( token ); - - if( !user ){ - return genErrorResponse( "Invalid Session Token", 401 ); - } - - const current_date = new Date(); - const iat = Math.floor( (current_date.getTime())/1000 ); - const renewal_exp = ( iat + 3600*jwtRenewalTimeout ) * 1000; - /** - * Renew session token on every login event. - * Previous session token is lost - */ - const session_token = toSha256( `${new Date()}` ); - const session_token_exp = new Date( renewal_exp ); - - await Repository.addSessionToken( user.id, session_token, session_token_exp ); - - const payload = { - iat: iat, - exp: iat + jwtTimeout * 3600, - aud: jwtOptions.audience, - iss: jwtOptions.audience, - sub: user.id, - }; - const jwt = jsonwebtoken.sign( payload , jwtSecret ); - return { - accessToken : jwt, - payload : payload, - session_token, - session_token_exp, - user : user - }; - } - -}; - -module.exports = new Account(); diff --git a/v2/server/src/Apps/Account/Interfaces/index.js b/v2/server/src/Apps/Account/Interfaces/index.js deleted file mode 100644 index 249a5de..0000000 --- a/v2/server/src/Apps/Account/Interfaces/index.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -const ModuleName = "App:Account:"; - -const { toSha256 } = require('../../../Shared/ShaUtils'); - -const { authentication } = require('../../../../config/apiConfig.json'); - -const { genErrorResponse } = require('../../../Shared/ErrorResponse'); - -const SharedResources = require('../../../Shared/Resources'); - -function registerEvent( event_id , callback ){ - console.log(`Loading event ${event_id}`); - const EventBus = SharedResources.get("SysS:EventManager"); - EventBus.addEvent( event_id , callback ); -} - -function publishEvent( event , data = null ){ - const EventBus = SharedResources.get("SysS:EventManager"); - const AppEventDomain = ModuleName; - const event_id = AppEventDomain + event; - - console.log( event_id ); - EventBus.publishEvent( event_id , data ); -} - -const tokenSecret = authentication.tokenSecret; -const jwtRenewalTimeout = authentication.jwtRenewalTimeout; -const jwtTimeout = authentication.jwtTimeout; -const jwtOptions = authentication.jwtOptions; -const jwtSecret = authentication.jwtSecret; - -const pwdSecret = authentication.pwdSecret; - -module.exports = { - toSha256, - tokenSecret, - jwtRenewalTimeout, - jwtTimeout, - jwtOptions, - jwtSecret, - pwdSecret, - genErrorResponse, - registerEvent, - publishEvent -}; diff --git a/v2/server/src/Apps/Account/Repository/Objection/index.js b/v2/server/src/Apps/Account/Repository/Objection/index.js deleted file mode 100644 index 14ebf19..0000000 --- a/v2/server/src/Apps/Account/Repository/Objection/index.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -const { getModel } = require('../../../../Shared/Models/Objection'); -const Users = getModel('users'); -const Sessions = getModel('users_sessions'); -const Companies = getModel('companies'); - -class SpecificModelRepository{ - constructor(){} - - async populate( user ){ - if( user.company_id ){ - const company = await Companies.query().findById( user.company_id ); - user.company = company; - }else{ - user.company = null; - } - return user; - } - - async getByEmail( email ){ - const user = await Users.query() - .select( "*" ) - .where("email","=",email).first(); - return user; - } - - async createOne( email, safe_password ){ - const user = await Users.query().insert({ - email, - password : safe_password, - "name":"No name", - "last_name":"No lastname", - "createdAt" : new Date().toISOString() - }); - return await this.populate( user ); - } - - async findByEmailPassword( email, safe_password ){ - const user = await Users.query().select('*') - .where("email","=",email) - .where("password","=",safe_password) - .first(); - if( user ){ - return await this.populate( user ); - }else{ - return null; - } - } - - async updateSessionToken( old_token, token, expiration ){ - const entry = await Sessions.query().select('*').where('token','=',old_token).first(); - const data = { - token, - expiration, - }; - if( entry ){ - return await Sessions.query().patch( data ).where('token','=',old_token).first(); - } - return null; - } - - async updatePassword( userId , safe_password ){ - return await Users.query() - .findById( userId ) - .patch( { password : safe_password } ); - } - - async addSessionToken( userId , token, expiration ){ - const entry = await Sessions.query().select('*').where("user_id",'=',userId).first(); - const data = { - token, - expiration : expiration.toISOString(), - }; - if( entry ){ - return await Sessions.query() - .findById( entry.id ) - .patch(data); - }else{ - data.user_id = userId; - return await Sessions.query().insert( data ); - } - } - - async findBySessionToken( token ){ - const session = await Sessions.query().select("*").where("token","=",token).first(); - const user = await Users.query().findById( session.user_id ); - if( user ){ - return await this.populate( user ); - }else{ - return null; - } - } - -} - -module.exports = new SpecificModelRepository(); \ No newline at end of file diff --git a/v2/server/src/Apps/Account/Repository/index.js b/v2/server/src/Apps/Account/Repository/index.js deleted file mode 100644 index 0c45820..0000000 --- a/v2/server/src/Apps/Account/Repository/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const SpecificModelRepository = require('./Objection'); - -module.exports = SpecificModelRepository; \ No newline at end of file diff --git a/v2/server/src/Apps/Company/Controller/hooks.js b/v2/server/src/Apps/Company/Controller/hooks.js deleted file mode 100644 index 6c8748a..0000000 --- a/v2/server/src/Apps/Company/Controller/hooks.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; -const Application = require('../Domain'); - -function dummy_middleware( req, res, next ){ - Application.trigger_example_event( { - example : "This is an example of event data" - } ) - return res.status(500).send({ error:"Not implemented yet" }); -} - -function AppInit(){ - Application.init(); -} - -module.exports = { - AppInit, - hooks : { - before: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [], - del : [], - get : [], - patch : [], - post : [], - put : [] - }, - - after: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [ { "/test" : dummy_middleware } ], - del : [], - get : [], - patch : [], - post : [], - put : [] - }, - } -}; diff --git a/v2/server/src/Apps/Company/Controller/index.js b/v2/server/src/Apps/Company/Controller/index.js deleted file mode 100644 index 7a3a6eb..0000000 --- a/v2/server/src/Apps/Company/Controller/index.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const { hooks, AppInit } = require( './hooks' ); -const GenericController = require('../../Lib'); - -class Company extends GenericController { - init(){ - super.init(); - AppInit(); - } -} - -module.exports = new Company( hooks ); diff --git a/v2/server/src/Apps/Company/Domain/index.js b/v2/server/src/Apps/Company/Domain/index.js deleted file mode 100644 index 5e334e6..0000000 --- a/v2/server/src/Apps/Company/Domain/index.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const Repository = require('../Repository'); -const Interfaces = require('../Interfaces'); - -class Company { - constructor(){ - this.Events = { - /** Event_Id : callback */ - "Example" : this.event_handler_example - }; - } - - init(){ - /// Setup application events - const Events = this.Events; - /// Setup application events - for ( const [event, callback] of Object.entries( Events ) ) { - const event_id = Interfaces.ModuleName + event - Interfaces.registerEvent( event_id , callback ); - } - } - - trigger_example_event( data ){ - Interfaces.publishEvent( "Example", data ); - } - - event_handler_example( data ){ - console.log( "CompanyDomain event" ); - console.log( data ); - } -}; - -module.exports = new Company(); diff --git a/v2/server/src/Apps/Company/Interfaces/index.js b/v2/server/src/Apps/Company/Interfaces/index.js deleted file mode 100644 index 751cd96..0000000 --- a/v2/server/src/Apps/Company/Interfaces/index.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const ModuleName = "App:Company:"; - -const SharedResources = require('../../../Shared/Resources'); - -function registerEvent( event_id , callback ){ - const EventBus = SharedResources.get("SysS:EventManager"); - - console.log(`Loading event ${event_id}`); - EventBus.addEvent( event_id , callback ); -} - -function publishEvent( event , data = null ){ - const EventBus = SharedResources.get("SysS:EventManager"); - const AppEventDomain = ModuleName; - const event_id = AppEventDomain + event; - - console.log( event_id ); - EventBus.publishEvent( event_id , data ); -} - -module.exports = { - ModuleName, - registerEvent, - publishEvent -}; diff --git a/v2/server/src/Apps/Company/Repository/Objection/index.js b/v2/server/src/Apps/Company/Repository/Objection/index.js deleted file mode 100644 index b278e23..0000000 --- a/v2/server/src/Apps/Company/Repository/Objection/index.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = {}; \ No newline at end of file diff --git a/v2/server/src/Apps/Company/Repository/index.js b/v2/server/src/Apps/Company/Repository/index.js deleted file mode 100644 index 0c45820..0000000 --- a/v2/server/src/Apps/Company/Repository/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const SpecificModelRepository = require('./Objection'); - -module.exports = SpecificModelRepository; \ No newline at end of file diff --git a/v2/server/src/Apps/Lib/index.js b/v2/server/src/Apps/Lib/index.js deleted file mode 100644 index 317362a..0000000 --- a/v2/server/src/Apps/Lib/index.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -const express = require('express'); - -class GenericController{ - constructor( hooks ){ - this.router = express.Router(); - this.hooks = hooks; - } - - init(){ - const { before, after } = this.hooks; - if( before ){ - this.populate_hooks( before ); - } - if( after ){ - this.populate_hooks( after ); - } - - return this; - } - - populate_hooks( hooks ){ - if( (hooks.all) && (hooks.all.length > 0) ){ - this.populate_hooks_by_verb( 'all' , hooks.all ); - } - - if( (hooks.delete) && (hooks.delete.length > 0) ){ - this.populate_hooks_by_verb( 'del', hooks.del ); - } - - if( (hooks.get) && (hooks.get.length > 0) ){ - this.populate_hooks_by_verb( 'get', hooks.get ); - } - - if( (hooks.patch) && (hooks.patch.length > 0) ){ - this.populate_hooks_by_verb( 'patch', hooks.patch ); - } - - if( (hooks.post) && (hooks.post.length > 0) ){ - this.populate_hooks_by_verb( 'post', hooks.post ); - } - - if( (hooks.put) && (hooks.put.length > 0) ){ - this.populate_hooks_by_verb( 'put', hooks.put ); - } - } - - add_hook_with_endpoint( http_verb, endpoint, middleware ){ - switch( http_verb ){ - case 'all': - this.router.use( endpoint , middleware ); - break; - case 'del': - this.router.delete(endpoint, middleware); - break; - case 'get': - this.router.get(endpoint, middleware); - break; - case 'patch': - this.router.patch(endpoint, middleware); - break; - case 'post': - this.router.post(endpoint, middleware); - break; - case 'put': - this.router.put(endpoint, middleware); - break; - default: - return; - } - } - - add_simple_hook( http_verb, middleware ){ - switch( http_verb ){ - case 'all': - this.router.use( middleware ); - break; - case 'del': - this.router.delete( middleware ); - break; - case 'get': - this.router.get( middleware ); - break; - case 'patch': - this.router.patch( middleware ); - break; - case 'post': - this.router.post( middleware ); - break; - case 'put': - this.router.put( middleware ); - break; - default: - return; - } - } - - populate_hooks_by_verb( http_verb, hook_list ){ - for (let custom_hook of hook_list ){ - if( typeof custom_hook === "object" ){ - for (let entry of Object.entries(custom_hook) ){ - const endpoint = entry[0]; - const callback = entry[1]; - /// Add this hook as an (endpoint, middleware function) pair - this.add_hook_with_endpoint( http_verb, endpoint, callback ); - } - }else{ - /// Add this hook as a middleware function - this.add_simple_hook( http_verb, custom_hook ); - } - } - } -} - -module.exports = GenericController; diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js b/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js deleted file mode 100644 index 264029c..0000000 --- a/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -const { DateResolver, DateTimeResolver } = require('graphql-scalars'); -const { Account, User, Company, getUserById, getCompanyById, findUsersPage, findCompaniesPage } = require('../../Domain'); - -////////////////////////////////////////////// -// Queries -////////////////////////////////////////////// -async function account( args, context ) { - const account = new Account( context.requestContext.userId ); - return account; -} - -async function profile( args, context ) { - const profile = new User( context.requestContext.userId ); - return profile; -} - -async function company( args, context ) { - const company = new Company( context.requestContext.companyId ); - return company; -} - -async function companyById( args, context ) { - return getCompanyById( args.id ); -} - -async function userById( args, context ) { - return getUserById( args.id ); -} - -async function findCompanies( args, context ) { - const { filters, elements, page } = args; - return findCompaniesPage( filters, elements, page ); -} - -async function findUsers( args, context ) { - const { filters, elements, page } = args; - return findUsersPage( filters, elements, page ); -} - -///////////////////////////////////////////////// -// Mutations -///////////////////////////////////////////////// - -module.exports = { - account, - profile, - company, - companyById, - userById, - findCompanies, - findUsers -}; diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql deleted file mode 100644 index 680510c..0000000 --- a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql +++ /dev/null @@ -1,146 +0,0 @@ -type Query { - account : Account! - profile : User! - company : Company! - - companyById( id : Int! ) : PublicCompany - userById( id : Int! ) : PublicUser - - findCompanies( filters : FindCompanyFilterInput!, elements: Int!, page: Int! ) : PublicCompanyFound! - findUsers( filters : FindUserFilterInput!, elements: Int!, page: Int! ) : PublicUserFound! -} - -scalar DateTime - -type Session { - token : String! - expiration : DateTime! -} - -type Account { - user : User! - - sessions( limit: Int , offset : Int ) : [Session]! -} - -type LocationCategory{ - id : Int! - category : String! -} - -type Location{ - id : Int! - company : Company! - type : String! - state : String! - city : String! - country : String! - zipcode : String! - address_line1 : String! - address_line2 : String - - categories : [LocationCategory]! -} - -type TruckType{ - id : Int! - category : String! -} - -type CompanyCategory{ - id : Int! - category : String! -} - -type Company { - id : Int! - owner : User! - staff : [User]! - - type : String! - is_hidden : Boolean! - is_active : Boolean! - - name : String! - description : String - - createdAt : DateTime! - - locations : [Location]! - categories : [CompanyCategory]! - truck_types : [TruckType]! -} - -type User { - id : Int! - company : Company - phone : String - - email : String! - name : String! - last_name : String! - job_role : String! - permissions : String! - createdAt : DateTime! - is_active : Boolean! - - locations( limit: Int , offset : Int ) : [Location]! -} - -type PublicCompany { - id : Int! - owner : PublicUser! - staff : [PublicUser]! - - type : String! - name : String! - description : String - - createdAt : DateTime! - - locations : [Location]! - categories : [CompanyCategory]! - truck_types : [TruckType]! -} - -type PublicCompanyFound { - count : Int! - list : [PublicCompany]! -} - -input FindCompanyFilterInput { - name : String - type : String - state : String - city : String - truck_type : String - categories : String -} - -type PublicUser { - id : Int! - company : Company - phone : String - - email : String! - name : String! - last_name : String! - job_role : String! - createdAt : DateTime! - - locations( limit: Int , offset : Int ) : [Location]! -} - -type PublicUserFound { - count : Int! - list : [PublicUser]! -} - -input FindUserFilterInput { - email : String - name : String - last_name : String - employee_id : String - company_name : String - job_role : String -} diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js deleted file mode 100644 index c528542..0000000 --- a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; -const fs = require('fs'); -const { join } = require('path'); -const { buildSchema } = require('graphql'); - -const schema = fs.readFileSync(join(__dirname, './schema.graphql'), 'utf8'); - -module.exports = buildSchema( schema ); diff --git a/v2/server/src/Apps/PrivateResources/Controller/hooks.js b/v2/server/src/Apps/PrivateResources/Controller/hooks.js deleted file mode 100644 index 8781b23..0000000 --- a/v2/server/src/Apps/PrivateResources/Controller/hooks.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const { createHandler } = require("graphql-http/lib/use/express"); - -/// Include graphql schema and resolvers -const schemaDescription = require('./graphql/schema.js'); -const schemaResolvers = require('./graphql/resolvers.js'); - -const { initEvents } = require('../Domain'); - -async function test(req, res, next){ - console.log( req.requestContext ); - res.status(200).send({ - msg : "It is alive!" - }); -} - -module.exports = { - AppInit : initEvents, /// Dummy App Init - hooks : { - before: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [], - del : [], - get : [ test ], - patch : [], - post : [], - put : [] - }, - - after: { - /**Array of middleware functions or objects lile { endpoint, middleware }*/ - all : [], - del : [], - get : [], - patch : [], - post : [ - { '/graphql' : - createHandler({ - schema: schemaDescription, - rootValue : schemaResolvers, - context: async (req, params) => { return { requestContext : req.raw.requestContext }; }, - graphiql: true - }) - } - ], - put : [] - } - } -}; diff --git a/v2/server/src/Apps/PrivateResources/Controller/index.js b/v2/server/src/Apps/PrivateResources/Controller/index.js deleted file mode 100644 index c718390..0000000 --- a/v2/server/src/Apps/PrivateResources/Controller/index.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const { hooks, AppInit } = require( './hooks' ); -const GenericController = require('../../Lib'); - -class PrivateResources extends GenericController { - init(){ - super.init(); - AppInit(); - } -} - -module.exports = new PrivateResources( hooks ); diff --git a/v2/server/src/Apps/PrivateResources/Domain/index.js b/v2/server/src/Apps/PrivateResources/Domain/index.js deleted file mode 100644 index 1ccfe01..0000000 --- a/v2/server/src/Apps/PrivateResources/Domain/index.js +++ /dev/null @@ -1,389 +0,0 @@ -'use strict'; - -const Repository = require('../Repository'); -const Interfaces = require('../Interfaces'); - -class TruckType { - constructor( category ){ - this._category = category; - } - - async id(){ - return this._category.id; - } - async category(){ - return this._category.category; - } -} - -class Category { - constructor( category ){ - this._category = category; - } - - async id(){ - return this._category.id; - } - async category(){ - return this._category.category; - } -} - -class Location { - constructor( location ){ - this._location = location; - this._categories = null; - } - - async populate_content(){ - if( !this._categories ){ - this._categories = await Repository.getCategories( 'location', this._location.id ); - - this._categories.map( (element) => { - return new Category( element ); - }); - } - } - - async company_id(){ - await this.populate_content(); - this._location.company_id; - } - async type(){ - await this.populate_content(); - this._location.type; - } - async state(){ - await this.populate_content(); - this._location.state; - } - async city(){ - await this.populate_content(); - this._location.city; - } - async country(){ - await this.populate_content(); - this._location.country; - } - async zipcode(){ - await this.populate_content(); - this._location.zipcode; - } - async address_line1(){ - await this.populate_content(); - this._location.address_line1; - } - async address_line2(){ - await this.populate_content(); - this._location.address_line2; - } - - async categories(){ - await this.populate_content(); - return this._categories; - } -} - -class Company { - constructor( companyId , owner = null ){ - this._companyId = companyId; - this._company = null; - this._owner = owner; - this._staff = null; - this._locations = null; - this._truck_types = null; - this._categories = null; - } - - setCompany( company ){ - this._company = company; - } - - async populate_content(){ - if(! this._company ){ - this._company = await Repository.getCompanyById( this._companyId ); - } - if(! this._owner ){ - this._owner = new User( this._company.owner_id, this ); - await this._owner.populate_content(); - } - if( !this._locations ){ - this._locations = await Repository.getLocations( 'company', this._companyId ); - - this._locations.map( (element) => { - return new Location( element ); - }); - } - if( !this._truck_types ){ - this._truck_types = await Repository.getTruckTypes( 'company', this._companyId ); - - this._truck_types.map( (element) => { - return new TruckType( element ); - }); - } - if( !this._categories ){ - this._categories = await Repository.getCategories( 'company', this._companyId ); - - this._categories.map( (element) => { - return new Category( element ); - }); - } - if( !this._staff ){ - /// ToDo: Populate _staff - this._staff = [] - } - } - - async id(){ - return this._companyId; - } - async owner(){ - await this.populate_content(); - return this._owner; - } - async staff(){ - await this.populate_content(); - return this._staff; - } - - async type(){ - await this.populate_content(); - return this._company.type; - } - async is_hidden(){ - await this.populate_content(); - return this._company.is_hidden; - } - async is_active(){ - await this.populate_content(); - return this._company.is_active; - } - async name(){ - await this.populate_content(); - return this._company.name; - } - async description(){ - await this.populate_content(); - return this._company.description; - } - async createdAt(){ - await this.populate_content(); - return this._company.createdAt; - } - - async locations(){ - await this.populate_content(); - return this._locations; - } - async categories(){ - await this.populate_content(); - return this._categories; - } - async truck_types(){ - await this.populate_content(); - return this._truck_types; - } -} - -class User { - constructor( userId, company = null ){ - this._userId = userId; - this._user = null; - this._locations = null; - - this._company = company; - } - - setUser( user ){ - this._user = user; - } - - async populate_content(){ - if(! this._user ){ - this._user = await Repository.getUserById( this._userId ); - } - - if( (!this._company) && (this._user.company_id) ){ - /// Populate only if company is linked to user - this._company = new Company( this._user.company_id, this ); - await this._company.populate_content(); - } - - if( !this._locations ){ - this._locations = await Repository.getLocations( 'user', this._userId ); - - this._locations.map( (element) => { - return new Location( element ); - }); - } - } - - async id(){ - return this._userId; - } - - async company(){ - await this.populate_content(); - return this._company; - } - async phone(){ - await this.populate_content(); - return this._user.phone; - } - - async email(){ - await this.populate_content(); - return this._user.email; - } - async name(){ - await this.populate_content(); - return this._user.name; - } - async last_name(){ - await this.populate_content(); - return this._user.last_name; - } - async job_role(){ - await this.populate_content(); - return this._user.job_role; - } - async permissions(){ - await this.populate_content(); - return this._user.permissions; - } - async createdAt(){ - await this.populate_content(); - return this._user.createdAt; - } - async is_active(){ - await this.populate_content(); - return this._user.is_active; - } - - async locations(){ - await this.populate_content(); - return this._locations; - } -} - -class Account { - constructor( userId ){ - this._userId = userId; - this._sessions = null; - this._user = null; - } - - setUser( user ){ - this._user = user; - } - - async user(){ - if( this._user ){ - return this._user; - } - - this._user = new User( this._userId ); - return this._user; - } - - async populate_content(){ - if( this._sessions ){ - return; - } - - this._sessions = await Repository.getSessions( this._userId ); - - this._sessions.map( (item) => { - return { - token : item.token, - expiration : item.expiration - }; - } ); - } - - async sessions(){ - await this.populate_content(); - return this._sessions; - } -}; - -async function getUserById( id ) { - const user = await Repository.getUserById( id ); - if( user ){ - const user_obj = new User( user.id ); - user_obj.setUser(user); - return user_obj; - }else{ - return null; - } -} - -async function getCompanyById( id ) { - const company = await Repository.getCompanyById( id ); - if( company ){ - const company_obj = new Company( user.id ); - company_obj.setCompany(company); - return company_obj; - }else{ - return null; - } -} - -async function findUsersPage( filters, elements, page ) { - /// ToDo: Populate find - const list = []; - list.map( (element) => { - const user = new User( element.id ); - user.setUser( element ); - return user; - } ); - return { - count : list.length, - list - }; -} - -async function findCompaniesPage( filters, elements, page ) { - /// ToDo: Populate find - const list = []; - list.map( (element) => { - const company = new Company( element.id ); - company.setCompany( element ); - return company; - } ); - return { - count : list.length, - list - }; -} - - -/** - * List of Events to handle in this App Domain - */ -const Events = { - /** Event_Id : callback */ -}; - -function initEvents(){ - console.log("Init GraphQL Events"); - /// Setup application events - for ( const [event, callback] of Object.entries( Events ) ) { - const event_id = Interfaces.ModuleName + event - Interfaces.registerEvent( event_id , callback ); - } -} - -module.exports = { - initEvents, - Account, - User, - Company, - Location, - Category, - TruckType, - getUserById, - getCompanyById, - findUsersPage, - findCompaniesPage, -}; diff --git a/v2/server/src/Apps/PrivateResources/Interfaces/index.js b/v2/server/src/Apps/PrivateResources/Interfaces/index.js deleted file mode 100644 index 3f2e9de..0000000 --- a/v2/server/src/Apps/PrivateResources/Interfaces/index.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const ModuleName = "App:PrivateResources:"; - -const SharedResources = require('../../../Shared/Resources'); - -function registerEvent( event_id , callback ){ - const EventBus = SharedResources.get("SysS:EventManager"); - - console.log(`Loading event ${event_id}`); - EventBus.addEvent( event_id , callback ); -} - -function publishEvent( event , data = null ){ - const EventBus = SharedResources.get("SysS:EventManager"); - const AppEventDomain = ModuleName; - const event_id = AppEventDomain + event; - - console.log( event_id ); - EventBus.publishEvent( event_id , data ); -} - -module.exports = { - ModuleName, - registerEvent, - publishEvent -}; diff --git a/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js b/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js deleted file mode 100644 index c90e3a2..0000000 --- a/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -const { getModel } = require('../../../../Shared/Models/Objection'); -const Users = getModel('users'); -const Sessions = getModel('users_sessions'); -const Companies = getModel('companies'); - -const Locations = getModel('locations'); -const userLocations = getModel('user_locations'); -const companyTruckTypes = getModel('company_truck_types'); -const companyCategories = getModel('company_categories'); -const locationTruckTypes = getModel('location_truck_types'); -const locationCategories = getModel('location_categories'); - -class SpecificModelRepository{ - constructor(){} - - async getUserById( userId ){ - return await Users.query().findById( userId ); - } - - async getCompanyById( companyId ){ - return await Companies.query().findById( companyId ); - } - - async getSessions( userId ){ - return await Sessions.query().where("user_id","=",userId); - } - - async getLocations( location_type, elementId ){ - switch( location_type ){ - case 'company': - return await Locations.query().where("company_id","=",elementId); - case 'user': - ///ToDo: Implement join on Locations and userLocations - return []; - default: - return []; - } - } - - async getCategories( category_type, elementId ){ - switch( category_type ){ - case 'company': - return await companyCategories.query().where("company_id","=",elementId); - case 'location': - ///ToDo: Implement join on companyCategories and locationCategories - return []; - default: - return []; - } - } - - async getTruckTypes( search_type, elementId ){ - switch( search_type ){ - case 'company': - return await companyTruckTypes.query().where("company_id","=",elementId); - case 'location': - ///ToDo: Implement join on companyTruckTypes and locationTruckTypes - return []; - default: - return []; - } - } - -} - -module.exports = new SpecificModelRepository(); \ No newline at end of file diff --git a/v2/server/src/Apps/PrivateResources/Repository/index.js b/v2/server/src/Apps/PrivateResources/Repository/index.js deleted file mode 100644 index 0c45820..0000000 --- a/v2/server/src/Apps/PrivateResources/Repository/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const SpecificModelRepository = require('./Objection'); - -module.exports = SpecificModelRepository; \ No newline at end of file diff --git a/v2/server/src/Controller/index.js b/v2/server/src/Controller/index.js deleted file mode 100644 index b92c3a6..0000000 --- a/v2/server/src/Controller/index.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; -/** - * ExpressJS Controller - */ -const express = require('express'); -const SystemService = require('../SysS/Lib'); - -const middlewares = require('./middlewares'); -const Resources = require('./resources'); - -class AppLayerController extends SystemService { - constructor(){ - super(); - this.app = express(); - this.resources = Resources; - } - - async setup(){ - super.setup(); - } - - async init(){ - super.init(); - - const app = this.app; - - /// Populate context from JWT payload - app.use( middlewares.jwtValidator ); - app.use( middlewares.contextGenerator ); - - for ( const resource of this.resources ){ - for ( const [endpoint, controller] of Object.entries( resource ) ) { - controller.init(); - app.use( endpoint, controller.router ); - } - } - - } - - app_controller(){ - return this.app; - } -} - -module.exports = new AppLayerController(); diff --git a/v2/server/src/Controller/middlewares.js b/v2/server/src/Controller/middlewares.js deleted file mode 100644 index b58c157..0000000 --- a/v2/server/src/Controller/middlewares.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const apiConfig = require( '../../config/apiConfig.json' ); -const jwt = require('jsonwebtoken'); -const jwtSecret = apiConfig.authentication.jwtSecret; - -const { getModel } = require('../Shared/Models/Objection'); -const Users = getModel('users'); - -function jwtValidator(req, res, next){ - if( req.JWT ){ - req.JWT.isValid = false; - try{ - req.JWT.payload = jwt.verify( req.JWT.raw, jwtSecret ); - if( !req.JWT.payload ){ - return res.status(401).send({error:"Unauthorized",code:401}); - }else{ - req.JWT.isValid = true; - } - }catch( err ){ - console.error( err ); - return res.status(401).send({error:"Unauthorized",code:401}); - } - return next(); - }else{ - /// If no JWT available, ignore and continue - return next(); - // return res.status(401).send({error:"Unauthorized",code:401}); - } -} - -async function contextGenerator( req, res, next ){ - if( ! req.JWT?.isValid ){ - /// If no JWT available, ignore and continue - return next(); - // return res.status(401).send({error:"Unauthorized",code:401}); - } - - /// Process only if JWT is valid - const userId = req.JWT.payload.sub; - const user = await Users.query().findById( userId ).select( "id", "company_id", "phone", "email", "name", "last_name", "job_role", "permissions", "createdAt", "is_active" ); - const companyId = user.company_id; - const job_role = user.job_role; - const permissions = user.permissions; - - req.requestContext = { - userId, - companyId, - job_role, - permissions, - user - } - - return next(); -} - -module.exports = { - jwtValidator, - contextGenerator -}; diff --git a/v2/server/src/Controller/resources.js b/v2/server/src/Controller/resources.js deleted file mode 100644 index 1516a99..0000000 --- a/v2/server/src/Controller/resources.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - - -const Account = require('../Apps/Account/Controller'); -const Company = require('../Apps/Company/Controller'); -const PrivateResources = require('../Apps/PrivateResources/Controller'); - -module.exports = [ - { "/account": Account }, - { "/company": Company }, - { "/private": PrivateResources }, -]; \ No newline at end of file diff --git a/v2/server/src/Shared/ErrorResponse.js b/v2/server/src/Shared/ErrorResponse.js deleted file mode 100644 index 0182dbb..0000000 --- a/v2/server/src/Shared/ErrorResponse.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -function genErrorResponse( msg , code = 400 ){ - return { - error : { - code, - msg - } - } -} - -module.exports = { genErrorResponse }; diff --git a/v2/server/src/Shared/Models/Objection/companies.model.js b/v2/server/src/Shared/Models/Objection/companies.model.js deleted file mode 100644 index 3571d59..0000000 --- a/v2/server/src/Shared/Models/Objection/companies.model.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Companies extends Model { - static get tableName() { return 'companies'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['owner_id','type','is_hidden','is_active','name','description','createdAt'], - properties : { - owner_id : { type : 'integer', minimum : 0 }, - type : { type : 'string' , enum : ['carrier', 'shipper'] }, - is_hidden : { type : 'boolean', default : true }, - is_active : { type : 'boolean', default : true }, - name : { type : 'string', maxLength : 100 }, - description : { type : 'string', maxLength : 256 }, - createdAt : { type : 'date-time' } - } - }; - } -} - -module.exports = Companies; diff --git a/v2/server/src/Shared/Models/Objection/company_categories.model.js b/v2/server/src/Shared/Models/Objection/company_categories.model.js deleted file mode 100644 index ade7e4a..0000000 --- a/v2/server/src/Shared/Models/Objection/company_categories.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class CompanyCategories extends Model { - static get tableName() { return 'company_categories'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['company_id','category'], - properties : { - company_id : { type : 'integer', minimum : 0 }, - category : { type : 'string', maxLength : 100 }, - } - }; - } -} - -module.exports = CompanyCategories; diff --git a/v2/server/src/Shared/Models/Objection/company_truck_types.model.js b/v2/server/src/Shared/Models/Objection/company_truck_types.model.js deleted file mode 100644 index 7e6b2cf..0000000 --- a/v2/server/src/Shared/Models/Objection/company_truck_types.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class CompanyTruckTypes extends Model { - static get tableName() { return 'company_truck_types'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['company_id','truck_type'], - properties : { - company_id : { type : 'integer', minimum : 0 }, - truck_type : { type : 'string', maxLength : 100 }, - } - }; - } -} - -module.exports = CompanyTruckTypes; diff --git a/v2/server/src/Shared/Models/Objection/index.js b/v2/server/src/Shared/Models/Objection/index.js deleted file mode 100644 index f972a80..0000000 --- a/v2/server/src/Shared/Models/Objection/index.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -const users = require('./users.model'); -const user_sessions = require('./user_sessions.model'); -const companies = require('./companies.model'); -const locations = require('./locations.model'); -const user_locations = require('./user_locations.model'); -const company_truck_types = require('./company_truck_types.model'); -const company_categories = require('./company_categories.model'); -const location_categories = require('./location_categories.model'); -const location_truck_types = require('./location_truck_types.model'); - -const metadata_categories = require('./metadata_categories.model'); -const metadata_cities = require('./metadata_cities.model'); -const metadata_products = require('./metadata_products.model'); -const metadata_truck_types = require('./metadata_truck_types.model'); - -function getModel( name ){ - switch( name ){ - case 'users': - return users; - case 'users_sessions': - return user_sessions; - case 'companies': - return companies; - case 'locations': - return locations; - - case 'user_locations': - return user_locations; - case 'company_truck_types': - return company_truck_types; - case 'company_categories': - return company_categories; - case 'location_categories': - return location_categories; - case 'location_truck_types': - return location_truck_types; - - case 'metadata_categories': - return metadata_categories; - case 'metadata_cities': - return metadata_cities; - case 'metadata_products': - return metadata_products; - case 'metadata_truck_types': - return metadata_truck_types; - default: - return null; - } -} - -module.exports = { - getModel -}; diff --git a/v2/server/src/Shared/Models/Objection/load_attachments.model.js b/v2/server/src/Shared/Models/Objection/load_attachments.model.js deleted file mode 100644 index 30b75b9..0000000 --- a/v2/server/src/Shared/Models/Objection/load_attachments.model.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class LoadAttachments extends Model { - static get tableName() { return 'load_attachments'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'status', - 'type', - 'createdAt', - 'updatedAt', - 'doneAt' - ], - properties : { - load_id : { type : 'integer' , minimum : 0 }, - shipper_id : { type : 'integer' , minimum : 0 }, - carrier_id : { type : 'integer' , minimum : 0 }, - author_id : { type : 'integer' , minimum : 0 }, - status : { type : 'string' , default : 'Draft', enum: ['Draft', 'Done'] }, - type : { type : 'string' , enum: ['Draft', 'Done'] }, - createdAt : { type : 'date-time' }, - updatedAt : { type : 'date-time' }, - doneAt : { type : 'date-time' } - } - }; - } -} - -module.exports = LoadAttachments; diff --git a/v2/server/src/Shared/Models/Objection/load_categories.model.js b/v2/server/src/Shared/Models/Objection/load_categories.model.js deleted file mode 100644 index 15f92b4..0000000 --- a/v2/server/src/Shared/Models/Objection/load_categories.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class LoadCategories extends Model { - static get tableName() { return 'load_categories'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['load_id','category_id'], - properties : { - load_id : { type : 'integer', minimum : 0 }, - category_id : { type : 'integer', minimum : 0 }, - } - }; - } -} - -module.exports = LoadCategories; diff --git a/v2/server/src/Shared/Models/Objection/loads.model.js b/v2/server/src/Shared/Models/Objection/loads.model.js deleted file mode 100644 index 3234819..0000000 --- a/v2/server/src/Shared/Models/Objection/loads.model.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Loads extends Model { - static get tableName() { return 'loads'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'company_id', - 'responsible_id', - 'origin_country', - 'origin_state', - 'origin_city', - 'origin_zipcode', - 'origin_address_line1', - 'destination_address_line1', - 'updatedAt', - 'createdAt', - 'publication_status' - ], - properties : { - company_id : { type : 'integer' , minimum : 0 }, - responsible_id : { type : 'integer' , minimum : 0 }, - truck_type : { type : 'string' , maxLength : 100 }, - origin_country : { type : 'string' , default : 'Mexico', maxLength : 45 }, - origin_state : { type : 'string' , maxLength : 45 }, - origin_city : { type : 'string' , maxLength : 45 }, - origin_zipcode : { type : 'string' , maxLength : 10 }, - origin_lat : { type : 'string' , maxLength : 45 }, - origin_lng : { type : 'string' , maxLength : 45 }, - origin_address_line1 : { type : 'string' , maxLength : 100 }, - origin_address_line2 : { type : 'string' , maxLength : 100 }, - destination_country : { type : 'string' , maxLength : 45 }, - destination_state : { type : 'string' , maxLength : 45 }, - destination_city : { type : 'string' , maxLength : 45 }, - destination_zipcode : { type : 'string' , maxLength : 10 }, - destination_lat : { type : 'string' , maxLength : 45 }, - destination_lng : { type : 'string' , maxLength : 45 }, - destination_address_line1 : { type : 'string' , maxLength : 100 }, - destination_address_line2 : { type : 'string' , maxLength : 100 }, - weight : { type : 'number' , minimum : 0.0 }, - est_loading_date : { type : 'date-time' }, - est_unloading_date : { type : 'date-time' }, - notes : { type : 'string', maxLength : 256 }, - updatedAt : { type : 'date-time' }, - createdAt : { type : 'date-time' }, - publishedAt : { type : 'date-time' }, - loadedAt : { type : 'date-time' }, - transitAt : { type : 'date-time' }, - deliveredAt : { type : 'date-time' }, - publication_status : { type : 'string' , default : 'Draft', enum : ['Draft', 'Published', 'Completed', 'Closed'] }, - status : { type : 'string', enum : ['Published', 'Loading', 'Transit', 'Downloading', 'Delivered'] } - } - }; - } -} - -module.exports = Loads; diff --git a/v2/server/src/Shared/Models/Objection/location_categories.model.js b/v2/server/src/Shared/Models/Objection/location_categories.model.js deleted file mode 100644 index 9e66918..0000000 --- a/v2/server/src/Shared/Models/Objection/location_categories.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class LocationCategories extends Model { - static get tableName() { return 'location_categories'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['location_id','category_id'], - properties : { - location_id : { type : 'integer', minimum : 0 }, - category_id : { type : 'integer', minimum : 0 }, - } - }; - } -} - -module.exports = LocationCategories; diff --git a/v2/server/src/Shared/Models/Objection/location_truck_types.model.js b/v2/server/src/Shared/Models/Objection/location_truck_types.model.js deleted file mode 100644 index 5d91509..0000000 --- a/v2/server/src/Shared/Models/Objection/location_truck_types.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class LocationTruckTypes extends Model { - static get tableName() { return 'location_truck_types'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['location_id','truck_type_id'], - properties : { - location_id : { type : 'integer', minimum : 0 }, - truck_type_id : { type : 'integer', minimum : 0 }, - } - }; - } -} - -module.exports = LocationTruckTypes; diff --git a/v2/server/src/Shared/Models/Objection/locations.model.js b/v2/server/src/Shared/Models/Objection/locations.model.js deleted file mode 100644 index 799afd2..0000000 --- a/v2/server/src/Shared/Models/Objection/locations.model.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Locations extends Model { - static get tableName() { return 'locations'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['company_id','type','state','city','country','zipcode','address_line1'], - properties : { - company_id : { type : 'integer', minimum : 0 }, - type : { type : 'string' , enum : ['loading', 'unloading', 'both'] }, - state : { type : 'string', maxLength : 45 }, - city : { type : 'string', maxLength : 45 }, - country : { type : 'string', maxLength : 45 }, - zipcode : { type : 'string', maxLength : 10 }, - address_line1 : { type : 'string', maxLength : 100 }, - address_line2 : { type : 'string', maxLength : 100 } - } - }; - } -} - -module.exports = Locations; diff --git a/v2/server/src/Shared/Models/Objection/metadata_categories.model.js b/v2/server/src/Shared/Models/Objection/metadata_categories.model.js deleted file mode 100644 index 23c26c4..0000000 --- a/v2/server/src/Shared/Models/Objection/metadata_categories.model.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class MetadataCategories extends Model { - static get tableName() { return 'metadata_categories'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['category'], - properties : { - category: { type: 'string', maxLength: 100 }, - } - }; - } -} - -module.exports = MetadataCategories; diff --git a/v2/server/src/Shared/Models/Objection/metadata_cities.model.js b/v2/server/src/Shared/Models/Objection/metadata_cities.model.js deleted file mode 100644 index 5977ab6..0000000 --- a/v2/server/src/Shared/Models/Objection/metadata_cities.model.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class MetadataCities extends Model { - static get tableName() { return 'metadata_cities'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['city','state','country'], - properties : { - city: { type: 'string', maxLength: 100 }, - state: { type: 'string', maxLength: 100 }, - country: { type: 'string', maxLength: 100 }, - zipcode: { type: 'string', maxLength: 100 }, - } - }; - } -} - -module.exports = MetadataCities; diff --git a/v2/server/src/Shared/Models/Objection/metadata_products.model.js b/v2/server/src/Shared/Models/Objection/metadata_products.model.js deleted file mode 100644 index d6d795d..0000000 --- a/v2/server/src/Shared/Models/Objection/metadata_products.model.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class MetadataProducts extends Model { - static get tableName() { return 'metadata_products'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['type'], - properties : { - product: { type: 'string', maxLength: 100 }, - } - }; - } -} - -module.exports = MetadataProducts; diff --git a/v2/server/src/Shared/Models/Objection/metadata_truck_types.model.js b/v2/server/src/Shared/Models/Objection/metadata_truck_types.model.js deleted file mode 100644 index 2c7f9be..0000000 --- a/v2/server/src/Shared/Models/Objection/metadata_truck_types.model.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class MetadataTruckTypes extends Model { - static get tableName() { return 'metadata_truck_types'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['type'], - properties : { - truck_type: { type: 'string', maxLength: 100 }, - } - }; - } -} - -module.exports = MetadataTruckTypes; diff --git a/v2/server/src/Shared/Models/Objection/user_locations.model.js b/v2/server/src/Shared/Models/Objection/user_locations.model.js deleted file mode 100644 index 892d83d..0000000 --- a/v2/server/src/Shared/Models/Objection/user_locations.model.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Users extends Model { - static get tableName() { return 'users'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'email', - 'password', - 'name', - 'last_name', - 'job_role', - 'permissions', - 'createdAt', - 'is_active' - ], - properties : { - company_id : { type : 'integer' , minimum : 0 }, - phone: { type: 'string' , maxLength : 45 }, - email : { type : 'string' , maxLength : 254 }, - password : { type : 'string' , maxLength : 64 }, - name : { type : 'string' , maxLength : 45 }, - last_name : { type : 'string', maxLength : 100 }, - job_role: { type: 'string', default : 'limited', enum : ['owner', 'manager', 'staff', 'driver', 'limited'] }, - permissions: { type: 'string', default: 'limited', enum : ['carrier', 'shipper', 'limited'] }, - createdAt: { type : 'date-time' }, - is_active : { type : 'boolean', default : true } - } - }; - } -} - -module.exports = Users; diff --git a/v2/server/src/Shared/Models/Objection/user_sessions.model.js b/v2/server/src/Shared/Models/Objection/user_sessions.model.js deleted file mode 100644 index b384971..0000000 --- a/v2/server/src/Shared/Models/Objection/user_sessions.model.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class UserSessions extends Model { - static get tableName() { return 'user_sessions'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['user_id','token','expiration'], - properties : { - user_id : { type : 'integer' , minimum : 0 }, - token: { type: 'string' , maxLength : 256 }, - expiration: { type: 'string' }, - } - }; - } -} - -module.exports = UserSessions; diff --git a/v2/server/src/Shared/Models/Objection/users.model.js b/v2/server/src/Shared/Models/Objection/users.model.js deleted file mode 100644 index 04b2578..0000000 --- a/v2/server/src/Shared/Models/Objection/users.model.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Users extends Model { - static get tableName() { return 'users'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'email', - 'password', - 'name', - 'last_name', - 'job_role', - 'permissions', - 'createdAt', - 'is_active' - ], - properties : { - company_id : { type : 'integer' , minimum : 0 }, - phone: { type: 'string' , maxLength : 45 }, - email : { type : 'string' , maxLength : 254 }, - password : { type : 'string' , maxLength : 64 }, - name : { type : 'string' , maxLength : 45 }, - last_name : { type : 'string', maxLength : 100 }, - job_role: { type: 'string', default : 'limited', enum : ['owner', 'manager', 'staff', 'driver', 'warehouse', 'limited'] }, - permissions: { type: 'string', default: 'limited', enum : ['carrier', 'shipper', 'limited'] }, - createdAt: { type: "string" }, - is_active : { type : 'boolean', default : true } - } - }; - } -} - -module.exports = Users; diff --git a/v2/server/src/Shared/Models/Objection/vechicle_publications.model.js b/v2/server/src/Shared/Models/Objection/vechicle_publications.model.js deleted file mode 100644 index ce7666d..0000000 --- a/v2/server/src/Shared/Models/Objection/vechicle_publications.model.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class VehiclePublications extends Model { - static get tableName() { return 'vehicle_publications'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'vehicle_id', - 'published_by', - 'published_date', - 'destination', - 'is_available', - 'is_hidden', - ], - properties : { - vehicle_id : { type : 'integer' , minimum : 0 }, - published_by : { type : 'integer' , minimum : 0 }, - published_date : { type : 'date-time' }, - destination : { type : 'string' , maxLength : 45 }, - is_available : { type: 'boolean' , default : false }, - is_hidden : { type: 'boolean' , default : true }, - available_date : { type : 'date-time' }, - notes : { type: 'string' , maxLength : 256 }, - } - }; - } -} - -module.exports = VehiclePublications; diff --git a/v2/server/src/Shared/Models/Objection/vehicle_categories.model.js b/v2/server/src/Shared/Models/Objection/vehicle_categories.model.js deleted file mode 100644 index 96cc57f..0000000 --- a/v2/server/src/Shared/Models/Objection/vehicle_categories.model.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class LoadCategories extends Model { - static get tableName() { return 'vehicle_categories'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : ['vehicle_id','category_id'], - properties : { - vehicle_id : { type : 'integer', minimum : 0 }, - category_id : { type : 'integer', minimum : 0 }, - } - }; - } -} - -module.exports = LoadCategories; diff --git a/v2/server/src/Shared/Models/Objection/vehicles.model.js b/v2/server/src/Shared/Models/Objection/vehicles.model.js deleted file mode 100644 index ecd1474..0000000 --- a/v2/server/src/Shared/Models/Objection/vehicles.model.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; -const { Model } = require('objection'); - -class Vehicles extends Model { - static get tableName() { return 'vehicles'; } - static get idColumn() { return 'id'; } - static get jsonSchema() { - return { - type : 'object', - required : [ - 'company_id', - 'background_tracking', - 'status', - 'createdAt' - ], - properties : { - company_id : { type : 'integer' , minimum : 0 }, - VIN : { type: 'string', maxLength : 20 }, - circulation_serial_number : { type: 'string', maxLength : 100 }, - truck_type : { type: 'string', maxLength : 100 }, - background_tracking: { type: 'boolean' , default : false }, - status: { type: 'string', default : 'Free', enum : ['Free', 'Loading', 'Transit', 'Downloading'] }, - last_location_lat : { type: 'string', maxLength : 45 }, - last_location_lng : { type: 'string', maxLength : 45 }, - last_location_time : { type : 'date-time' }, - active_load : { type : 'integer' , minimum : 0 }, - active_driver : { type : 'integer' , minimum : 0 }, - createdAt: { type : 'date-time' }, - } - }; - } -} - -module.exports = Vehicles; diff --git a/v2/server/src/Shared/Resources/index.js b/v2/server/src/Shared/Resources/index.js deleted file mode 100644 index 638a859..0000000 --- a/v2/server/src/Shared/Resources/index.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -class SharedResources{ - constructor(){ - this.dictionary = {}; - } - - set( key , val ){ - this.dictionary[ key ] = val; - } - - exists( key ){ - return ( this.dictionary[ key ] != undefined ); - } - - get( key ){ - if( ! this.exists( key ) ){ - throw new Error( `Key [${key}] not defined!` ); - } - return this.dictionary[ key ]; - } - - try_get( key ){ - if( ! this.exists( key ) ){ - return null; - }else{ - return this.dictionary[ key ]; - } - } - - remove( key ){ - if( this.exists(key) ){ - delete this.dictionary[ key ]; - } - } -} - -module.exports = new SharedResources(); \ No newline at end of file diff --git a/v2/server/src/Shared/ShaUtils.js b/v2/server/src/Shared/ShaUtils.js deleted file mode 100644 index 7804f6b..0000000 --- a/v2/server/src/Shared/ShaUtils.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; -const crypto = require('crypto'); -/** - * Convert string to sha256 string in hex - * @param {*} text - * @returns - */ -function toSha256( text ){ - return crypto.createHmac( "sha256" , "" ).update( text ).digest( 'hex' ); -} - -module.exports = { toSha256 }; diff --git a/v2/server/src/SysS/Connections/index.js b/v2/server/src/SysS/Connections/index.js deleted file mode 100644 index a476d39..0000000 --- a/v2/server/src/SysS/Connections/index.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; -const apiConfig = require( '../../../config/apiConfig.json' ); -const Knex = require('knex'); -const { Model } = require('objection'); - -const UNINIT = 0; -const INIT = 1; -const ONLINE = 2; -const OFFLINE = 3; - -class SystemServices { - constructor(){ - this.SystemServiceState = UNINIT; - } - - async setup(){ - this.SystemServiceState = UNINIT; - } - - async init(){ - this.SystemServiceState = INIT; - } - - async connect(){ - const knex = Knex({ - client: 'mysql', - useNullAsDefault: true, - connection: { - host: apiConfig.sql.host, - port: apiConfig.sql.port, - user: apiConfig.sql.user, - password: apiConfig.sql.password, - database: apiConfig.sql.database, - } - }); - Model.knex(knex); - this.knex = knex; - console.log("Connected to SQL"); - this.SystemServiceState = ONLINE; - } - - async disconnect(){ - this.knex.destroy(); - - console.log("SQL Disconnected"); - this.SystemServiceState = OFFLINE; - } - - async deinit(){ - this.SystemServiceState = UNINIT; - } - - async getState(){ - switch( this.SystemServiceState ){ - case UNINIT: - return "UNINIT"; - case INIT: - return "INIT"; - case ONLINE: - return "ONLINE"; - case OFFLINE: - return "OFFLINE"; - default: - return "UNINIT"; - } - } -} - -module.exports = new SystemServices(); diff --git a/v2/server/src/SysS/Controller/index.js b/v2/server/src/SysS/Controller/index.js deleted file mode 100644 index 93e7b02..0000000 --- a/v2/server/src/SysS/Controller/index.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; -/** - * ExpressJS Controller - */ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const compression = require('compression'); -const morgan = require('morgan'); -const helmet = require('helmet'); -const bodyParser = require('body-parser'); -const fileUpload = require('express-fileupload'); - -/// Import Applications to serve -const AppsController = require('../../Controller'); -const middlewares = require('./middlewares'); - -const UNINIT = 0; -const INIT = 1; -const ONLINE = 2; -const OFFLINE = 3; - -class ExpressJSServices { - constructor(){ - this.SystemServiceState = UNINIT; - this.serverPort = process.env.SERVER_PORT || 3000; - this.app = express(); - } - - async setup(){ - const app = this.app; - - await AppsController.setup(); - - app.use( middlewares.Auth ); - - app.use( - fileUpload({ - limits: { fileSize: 4 * 1024 * 1024 }, - abortOnLimit: true, - limitHandler: (req,res,next) => { - req.limitSize = true; - }, - }) - ); - - app.use((req, res, next) => { - if (req.limitSize) { - res.status(413).send({message:"File size limit has been reached",status:"PAYLOAD_TOO_LARGE"}); - }else{ - next() - } - - }); - - app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); - app.use(bodyParser.json({ limit: '50mb' })); - app.use(morgan('dev')); - - app.use(helmet({ - crossOriginResourcePolicy: false - })); - - app.use(compression()); - - app.use(cors({ - origin: '*', - methods: [ - 'GET', - 'POST', - 'PATCH', - 'PUT', - 'DELETE' - ], - allowedHeaders: ['Content-Type', 'Authorization'] - })); - - this.SystemServiceState = UNINIT; - } - - async init(){ - const app = this.app; - - await AppsController.init(); - - app.use( middlewares.errorJSON ); - app.use( AppsController.app_controller() ); - app.use( middlewares.error404 ); - - this.SystemServiceState = INIT; - } - - async connect(){ - const app = this.app; - const serverPort = this.serverPort; - - await AppsController.connect(); - - const server = app.listen( serverPort , function(err){ - if( !err ){ - console.log('API listen on port', serverPort ); - }else{ - console.log( err ); - } - }); - - this.server = server; - - this.SystemServiceState = ONLINE; - } - - async disconnect(){ - await AppsController.disconnect(); - this.server.close(); - - this.SystemServiceState = OFFLINE; - } - - async deinit(){ - await AppsController.deinit(); - this.SystemServiceState = UNINIT; - } - - async getState(){ - switch( this.SystemServiceState ){ - case UNINIT: - return "UNINIT"; - case INIT: - return "INIT"; - case ONLINE: - return "ONLINE"; - case OFFLINE: - return "OFFLINE"; - default: - return "UNINIT"; - } - } -} - -module.exports = new ExpressJSServices(); diff --git a/v2/server/src/SysS/Controller/middlewares.js b/v2/server/src/SysS/Controller/middlewares.js deleted file mode 100644 index a56b25f..0000000 --- a/v2/server/src/SysS/Controller/middlewares.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; -/** -* HASH -***************************************************** -* DEPENDENCIES -***************************************************** -* Based on Express Framework -* System -***************************************************** -* PUBLIC METHODS -***************************************************** -* Auth( req, res, next) -* Extract JWT or BasicAuth data -* errorJSON( error , request , response , next ) -* Generate error response on bad JSON format -* error404( request , response , next ) -* Generate error 404 response -* apiKey( request , response , next ) -* Generate error on invalid apikey -**/ - -/// Extract JWT or BasicAuth -function Auth( req, res , next ){ - /// - /// Try to extract the authorization data from headers - /// - let auth; - if( req.headers.hasOwnProperty( "authorization" ) ){ - auth = req.headers.authorization; - auth = auth.split(" ")[1]; - if( !auth ){ console.log( "NO HEADER AUTH available" ); return next(); } - //console.log( auth ); - /// Try BasicAuth { - try{ - let ba = Buffer.from( auth , 'base64' ).toString() - //const [user,pass] = ba.split(':'); - ba = ba.split(':'); - if( ba.length == 2 ){ - req.basicAuth = { user : ba[0] , password : ba[1] }; - } - }catch(error){ - console.log("MIDDLEWARE_AUTH_ERR_BA",error); - } - /// Try BasicAuth } - }else if( req.query.access_token ){ - auth = req.query.access_token; - if( !auth ){ console.log( "NO QUERY AUTH available" ); return next(); } - } - if( auth ){ - /// Try JWT { - try{ - let jwt = auth.split("."); - if( jwt.length == 3 ){ - req.JWT = {}; - req.JWT.raw = auth; - } - }catch( error ){ - console.log("MIDDLEWARE_AUTH_ERR_JWT",error); - } - /// Try JWT } - } - next(); -} - -function errorJSON( error , request , response , next ){ - console.log(error); - if( error !== null ){ - /// For body-parser errors - if( error instanceof SyntaxError && error.status === 400 && 'body' in error ){ - return response.status(400).json({ error : 'Invalid json' , code : 400 }); - } - /// For any error - return response.status(500).send( { error: "Internal server error" , code : 500 } ); - }else{ - return next(); - } -} - -function error404( request , response , next ){ - return response.status(404).send( { error : "Page not found", code : 404 } ); -} - -module.exports = { - Auth, - errorJSON, - error404, -}; diff --git a/v2/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js b/v2/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js deleted file mode 100644 index f2296d4..0000000 --- a/v2/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js +++ /dev/null @@ -1,84 +0,0 @@ -'user strict'; -const apiConfig = require( '../../../../config/apiConfig.json' ); -const nodemailer = require("nodemailer"); -const SendGrid = require("nodemailer-sendgrid"); - -const SiteName = "ETA Viaporte"; - -const sendgridConfig = apiConfig.sendgrid; - -const transporter = nodemailer.createTransport( - SendGrid({ - host: sendgridConfig.HOST, - apiKey: sendgridConfig.API_KEY - }) -); - -async function sendMailTemplate( templateId, receiver, subject, content ){ - /**TODO: Remove in production */ - const default_mail_list = [ - {pattern:"testing@etaviaporte.com",redirect:"testing@etaviaporte.com"}, - {pattern:"alex@etaviaporte.com",redirect:"alexandro_uribe@outlook.com"}, - {pattern:"pablo@etaviaporte.com",redirect:"josepablo134@gmail.com"} - ]; - for( let i=0; i< default_mail_list.length; i++ ){ - if( receiver.indexOf( default_mail_list[i].pattern ) >= 0 ){ - receiver = default_mail_list[i].redirect; - break;/** Set only the first match */ - } - } - return await transporter.sendMail({ - from: sendgridConfig.FROM, - to: receiver, - subject: subject, - templateId: templateId, - dynamic_template_data: content - }); -} - -async function AccountVerifyEmail( receiver , content ){ - const templateId = "d-e9b7966303694964a64b6e4954e9715d"; - const subject = "[ETA] Account Verification"; - const content_to_send = { - project_name: SiteName, - user_name: content.user_name, - user_email: receiver, - OTP : content.OTP - }; - return await sendMailTemplate( templateId, receiver, subject, content_to_send ); -} - -async function AccountConfirmed( receiver, content ){ - const templateId = "d-4daaab1b85d443ceba38826f606e9931"; - const subject = "[ETA] Welcome to ETA"; - const content_to_send = { - user_name: content.user_name, - }; - return await sendMailTemplate( templateId, receiver, subject, content_to_send ); -} - -async function AccountPwdResetEmail( receiver, content ){ - const templateId = "d-e9b7966303694964a64b6e4954e9715d"; - const subject = "[ETA] Password Reset"; - const content_to_send = { - project_name: SiteName, - user_name: content.user_name, - user_email: receiver, - OTP : content.OTP - }; - return await sendMailTemplate( templateId, receiver, subject, content_to_send ); -} - -async function ContactEmail( receiver, content ){ - const templateId = "d-1090dda1091442f3a75ee8ab39ad0f10"; - const subject = "[ETA] Contact Email"; - const content_to_send = { - project_name: SiteName, - user_name: content.name, - user_email: receiver - }; - return await sendMailTemplate( templateId, receiver, subject, content_to_send ); -} -//ContactEmail( "josepablo134@gmail.com", { email : "josepablo134@gmail.com", name:"Josepablo C.", message: "This is an example" } ).then().catch(); - -module.exports = { AccountVerifyEmail, AccountConfirmed, AccountPwdResetEmail, ContactEmail }; diff --git a/v2/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js b/v2/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js deleted file mode 100644 index f48ce78..0000000 --- a/v2/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js +++ /dev/null @@ -1,22 +0,0 @@ -'user strict'; -const nodemailer = require("nodemailer"); -const apiConfig = require( '../../../../config/apiConfig.json' ); - -const transporter = nodemailer.createTransport( - apiConfig.email_standalone -); - -async function StandAloneContactEmail( content ){ - const default_from = apiConfig.email_standalone.auth.user; - const receiver = "support@etaviaporte.com"; - const {name, email, message } = content; - return await transporter.sendMail({ - from: `${name} <${default_from}>`, - to: receiver, - subject: "Contact Email From Landing Page", - text: `\n\n The following is an email from : ${email}\n\n\n` + message - }); -} -//StandAloneContactEmail( { email : "josepablo134@gmail.com", name:"Josepablo C.", message: "This is an example" } ).then().catch(); - -module.exports = { StandAloneContactEmail }; diff --git a/v2/server/src/SysS/EventManager/EmailEvents/index.js b/v2/server/src/SysS/EventManager/EmailEvents/index.js deleted file mode 100644 index 98af238..0000000 --- a/v2/server/src/SysS/EventManager/EmailEvents/index.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const { StandAloneContactEmail } = require('./StandAlone.handler'); -const { AccountVerifyEmail, AccountConfirmed, AccountPwdResetEmail, ContactEmail } = require('./SendGrid.handler'); - -async function onChecksumGeneration( data ){ - console.log( data ); - const receiver = data.email; - await AccountVerifyEmail( receiver, data ); -} - -async function onAccountConfirmed( data ){ - const receiver = data.email; - await AccountConfirmed( receiver, data ); -} - -async function onPasswordReset( data ){ - const receiver = data.email; - await AccountPwdResetEmail( receiver, data ); -} - -async function onContactFromWebPage( data ){ - const receiver = data.email; - await StandAloneContactEmail( data ); - await ContactEmail( receiver, data ); -} - -/** - * Dictionary of event ids and handlers - */ -module.exports = { - "App:Account:getchecksum:signup" : onChecksumGeneration, - "App:Account:signupconfirmed":onAccountConfirmed, - "App:Account:getchecksum:recover":onPasswordReset, - "App:ContactEmail:contact":onContactFromWebPage, -}; diff --git a/v2/server/src/SysS/EventManager/index.js b/v2/server/src/SysS/EventManager/index.js deleted file mode 100644 index 76d9152..0000000 --- a/v2/server/src/SysS/EventManager/index.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; -const events = require('events'); -const SharedResources = require('../../Shared/Resources'); - -const resources_list = require('./resources'); - -const UNINIT = 0; -const INIT = 1; -const ONLINE = 2; -const OFFLINE = 3; - -class SystemServices { - constructor(){ - this.resources = resources_list; - this.eventEmitter = new events.EventEmitter(); - - this.SystemServiceState = UNINIT; - } - - addEvent( event_id , callback ){ - this.eventEmitter.on( event_id , callback ); - } - - publishEvent( event_id , data ){ - this.eventEmitter.emit( event_id , data ); - } - - async setup(){ - for ( const resource of this.resources ){ - for ( const [key, value] of Object.entries( resource ) ) { - this.eventEmitter.on( key , value ); - } - } - - this.SystemServiceState = UNINIT; - } - - async init(){ - SharedResources.set( "SysS:EventManager" , this ); - this.SystemServiceState = INIT; - } - - async connect(){ - this.SystemServiceState = ONLINE; - } - - async disconnect(){ - this.SystemServiceState = OFFLINE; - } - - async deinit(){ - this.SystemServiceState = UNINIT; - } - - async getState(){ - switch( this.SystemServiceState ){ - case UNINIT: - return "UNINIT"; - case INIT: - return "INIT"; - case ONLINE: - return "ONLINE"; - case OFFLINE: - return "OFFLINE"; - default: - return "UNINIT"; - } - } -} - -module.exports = new SystemServices(); diff --git a/v2/server/src/SysS/EventManager/resources.js b/v2/server/src/SysS/EventManager/resources.js deleted file mode 100644 index a488db6..0000000 --- a/v2/server/src/SysS/EventManager/resources.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const EmailEvents = require('./EmailEvents'); - -module.exports = [ - EmailEvents, -]; \ No newline at end of file diff --git a/v2/server/src/SysS/Lib/index.js b/v2/server/src/SysS/Lib/index.js deleted file mode 100644 index 4de0739..0000000 --- a/v2/server/src/SysS/Lib/index.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const UNINIT = 0; -const INIT = 1; -const ONLINE = 2; -const OFFLINE = 3; - -class SystemService { - constructor(){ - this.SystemServiceState = UNINIT; - } - - async setup(){ - this.SystemServiceState = UNINIT; - } - - async init(){ - this.SystemServiceState = INIT; - } - - async connect(){ - this.SystemServiceState = ONLINE; - } - - async disconnect(){ - this.SystemServiceState = OFFLINE; - } - - async deinit(){ - this.SystemServiceState = UNINIT; - } -} - -module.exports = SystemService; diff --git a/v2/server/src/SysS/index.js b/v2/server/src/SysS/index.js deleted file mode 100644 index 4d3904e..0000000 --- a/v2/server/src/SysS/index.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; -const EventManager = require('./EventManager'); -const Connections = require('./Connections'); -const Controller = require('./Controller'); - -const UNINIT = 0; -const INIT = 1; -const ONLINE = 2; -const OFFLINE = 3; - -class SystemServices { - constructor(){ - this.resources = [ - EventManager, - Connections, - Controller, - ]; - - this.SystemServiceState = UNINIT; - } - - async setup(){ - for ( const service of this.resources ){ - await service.setup(); - } - - this.SystemServiceState = UNINIT; - } - - async init(){ - for ( const service of this.resources ){ - await service.init(); - } - - this.SystemServiceState = INIT; - } - - async connect(){ - for ( const service of this.resources ){ - await service.connect(); - } - - this.SystemServiceState = ONLINE; - } - - async disconnect(){ - for ( const service of this.resources ){ - await service.disconnect(); - } - - this.SystemServiceState = OFFLINE; - } - - async deinit(){ - for ( const service of this.resources ){ - await service.deinit(); - } - - this.SystemServiceState = UNINIT; - } - - async getState(){ - switch( this.SystemServiceState ){ - case UNINIT: - return "UNINIT"; - case INIT: - return "INIT"; - case ONLINE: - return "ONLINE"; - case OFFLINE: - return "OFFLINE"; - default: - return "UNINIT"; - } - } -} - -module.exports = new SystemServices(); diff --git a/v2/server/src/index.js b/v2/server/src/index.js deleted file mode 100644 index f15c00e..0000000 --- a/v2/server/src/index.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; -const process = require('node:process'); -const SystemServices = require('./SysS'); - -async function main(){ - await SystemServices.setup(); - await SystemServices.init(); - await SystemServices.connect(); -} - -main() -.then( ( out ) => { if( out ){ console.log( out ); } } ) -.catch( ( error ) => { if( error ){ console.error( error ); } } ); - -async function disconnect_server( code ){ - if( await SystemServices.getState() !== "OFFLINE" ){ - await SystemServices.disconnect(); - console.log("Server disconnected with exit code : " , code ); - } -} - -process.on('SIGINT', () => { - disconnect_server( "SIGINT" ).then( ()=>{} ).catch((error)=>{ - console.error("Shutdown error", error); - }) -} ); - -process.on('exit', (code) => { - disconnect_server( "exit:"+code ).then( ()=>{} ).catch((error)=>{ - console.error("Shutdown error", error); - }) -} ); diff --git a/v2/server/test/index.js b/v2/server/test/index.js deleted file mode 100644 index 126d321..0000000 --- a/v2/server/test/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/// Unit testing dependences -const assert = require("assert"); -const SharedResources = require('../src/Shared/SharedResources'); - - -describe('Shared Resources' , () => { - it('Check key', async () => { - assert.equal( SharedResources.exists("test") , false ); - - SharedResources.set("test" , "This is a test value "); - assert.equal( SharedResources.exists("test") , true ); - - SharedResources.remove("test"); - assert.equal( SharedResources.exists("test") , false ); - }) -}); -