From 49673d811c42b1af03a24a6488eb88b3c65fdbba Mon Sep 17 00:00:00 2001 From: Josepablo C Date: Fri, 12 Apr 2024 12:22:02 -0600 Subject: [PATCH] feat(v3): Adding baseline for API v3 --- package.json | 6 +- server/README.md | 5 + server/config/apiConfig.json | 55 +++ server/docs/assets/APIDesign.png | Bin 0 -> 61993 bytes server/src/Apps/Account/Controller/index.js | 60 ++++ server/src/Apps/Account/Domain/index.js | 133 +++++++ server/src/Apps/Account/Ports/Events/index.js | 9 + .../Apps/Account/Ports/Interfaces/index.js | 38 ++ server/src/Apps/Account/Ports/Public/index.js | 3 + .../Account/Repository/Objection/index.js | 76 ++++ server/src/Apps/Account/Repository/index.js | 5 + server/src/Controller/index.js | 8 + 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 + server/src/Shared/Models/Objection/index.js | 22 ++ .../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 ++ server/src/Shared/Resources/index.js | 38 ++ server/src/Shared/ShaUtils.js | 12 + server/src/SysS/Connections/index.js | 68 ++++ server/src/SysS/Controller/index.js | 132 +++++++ server/src/SysS/Controller/middlewares.js | 87 +++++ .../EmailEvents/SendGrid.handler.js | 84 +++++ .../EmailEvents/StandAlone.handler.js | 22 ++ .../SysS/EventManager/EmailEvents/index.js | 36 ++ server/src/SysS/EventManager/index.js | 71 ++++ server/src/SysS/EventManager/resources.js | 7 + server/src/SysS/Template/index.js | 33 ++ server/src/SysS/index.js | 78 ++++ server/src/index.js | 32 ++ server/test/index.js | 18 + src/apps/private/vehicles/services.js | 336 +++++++++--------- 48 files changed, 1804 insertions(+), 170 deletions(-) create mode 100644 server/README.md create mode 100644 server/config/apiConfig.json create mode 100644 server/docs/assets/APIDesign.png create mode 100644 server/src/Apps/Account/Controller/index.js create mode 100644 server/src/Apps/Account/Domain/index.js create mode 100644 server/src/Apps/Account/Ports/Events/index.js create mode 100644 server/src/Apps/Account/Ports/Interfaces/index.js create mode 100644 server/src/Apps/Account/Ports/Public/index.js create mode 100644 server/src/Apps/Account/Repository/Objection/index.js create mode 100644 server/src/Apps/Account/Repository/index.js create mode 100644 server/src/Controller/index.js create mode 100644 server/src/Shared/ErrorResponse.js create mode 100644 server/src/Shared/Models/Objection/companies.model.js create mode 100644 server/src/Shared/Models/Objection/company_categories.model.js create mode 100644 server/src/Shared/Models/Objection/company_truck_types.model.js create mode 100644 server/src/Shared/Models/Objection/index.js create mode 100644 server/src/Shared/Models/Objection/load_attachments.model.js create mode 100644 server/src/Shared/Models/Objection/load_categories.model.js create mode 100644 server/src/Shared/Models/Objection/loads.model.js create mode 100644 server/src/Shared/Models/Objection/location_categories.model.js create mode 100644 server/src/Shared/Models/Objection/location_truck_types.model.js create mode 100644 server/src/Shared/Models/Objection/locations.model.js create mode 100644 server/src/Shared/Models/Objection/metadata_categories.model.js create mode 100644 server/src/Shared/Models/Objection/metadata_cities.model.js create mode 100644 server/src/Shared/Models/Objection/metadata_products.model.js create mode 100644 server/src/Shared/Models/Objection/metadata_truck_types.model.js create mode 100644 server/src/Shared/Models/Objection/user_locations.model.js create mode 100644 server/src/Shared/Models/Objection/user_sessions.model.js create mode 100644 server/src/Shared/Models/Objection/users.model.js create mode 100644 server/src/Shared/Models/Objection/vechicle_publications.model.js create mode 100644 server/src/Shared/Models/Objection/vehicle_categories.model.js create mode 100644 server/src/Shared/Models/Objection/vehicles.model.js create mode 100644 server/src/Shared/Resources/index.js create mode 100644 server/src/Shared/ShaUtils.js create mode 100644 server/src/SysS/Connections/index.js create mode 100644 server/src/SysS/Controller/index.js create mode 100644 server/src/SysS/Controller/middlewares.js create mode 100644 server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js create mode 100644 server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js create mode 100644 server/src/SysS/EventManager/EmailEvents/index.js create mode 100644 server/src/SysS/EventManager/index.js create mode 100644 server/src/SysS/EventManager/resources.js create mode 100644 server/src/SysS/Template/index.js create mode 100644 server/src/SysS/index.js create mode 100644 server/src/index.js create mode 100644 server/test/index.js diff --git a/package.json b/package.json index edb4198..399d07c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node src/", "dev": "nodemon src/", - "test":"mocha test/lib/handlers/proposals" + "dev3": "nodemon server/src/", + "test": "mocha test/lib/handlers/proposals" }, "repository": { "type": "git", @@ -38,10 +39,11 @@ "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.2", + "objection": "^3.1.4", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..3af4002 --- /dev/null +++ b/server/README.md @@ -0,0 +1,5 @@ +# REST API Framework + +## Architecture Design + +![alt text](docs/assets/APIDesign.png) \ No newline at end of file diff --git a/server/config/apiConfig.json b/server/config/apiConfig.json new file mode 100644 index 0000000..606c680 --- /dev/null +++ b/server/config/apiConfig.json @@ -0,0 +1,55 @@ +{ + "authentication": { + "pwdSecret":"Nx2g_IWo2Zt_LS$+", + "jwtSecret":"9o3BBz0EsrwXliwEJ/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" : "1.1.1", + "name": "ETA Beta", + "date":"03/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/server/docs/assets/APIDesign.png b/server/docs/assets/APIDesign.png new file mode 100644 index 0000000000000000000000000000000000000000..de582c84e642872ecb33347627e9a84bc9ea22fa GIT binary patch literal 61993 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≰hr;&a}Es!-TmNmNm!tIkWPq7*Ri8qC7f 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 G=)V9v8ojmv literal 0 HcmV?d00001 diff --git a/server/src/Apps/Account/Controller/index.js b/server/src/Apps/Account/Controller/index.js new file mode 100644 index 0000000..60e4312 --- /dev/null +++ b/server/src/Apps/Account/Controller/index.js @@ -0,0 +1,60 @@ +'use strict'; +/// Router instance +const router = require('express').Router(); +const Application = require('../Domain'); + +function dummy_middleware( req, res ){ + return res.status(500).send({ error:"Not implemented yet" }); +} + +router.post('/register', dummy_middleware ); + +router.post('/authorize', dummy_middleware ); + +router.get('/authorize/:session_token', dummy_middleware ); + +router.get('/check-account/:email', async( req, res ) => { + try{ + const email = req.params.email; + const data = await Application.check_account( email ); + return res.send( data ); + }catch(error){ + console.error( error ); + } +} ); + +router.post('/signup', async(req,res) => { + try{ + const email = req.body.email; + const password = req.body.password; + const data = await Application.getVerifyChecksum( 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 ); + } +} ); +router.patch('/signup', async(req,res) => { + 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 ); + } +} ); + +router.post('/recover', dummy_middleware ); +router.patch('/recover', dummy_middleware ); + +module.exports = router; diff --git a/server/src/Apps/Account/Domain/index.js b/server/src/Apps/Account/Domain/index.js new file mode 100644 index 0000000..42ce032 --- /dev/null +++ b/server/src/Apps/Account/Domain/index.js @@ -0,0 +1,133 @@ +'use strict'; + +const Repository = require('../Repository'); +const jsonwebtoken = require('jsonwebtoken'); +const { toSha256, + publishEvent, + jwtRenewalTimeout, + jwtTimeout, + jwtOptions, + jwtSecret, + tokenSecret, + pwdSecret, + genErrorResponse } = require('../Ports/Interfaces'); + +class ApplicationLogic { + constructor(){ + } + + 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 getVerifyChecksum( email , password ){ + const otp = this.genOTP( email ); + const it_exists = await Repository.getByEmail( email ); + + if( it_exists ){ + return genErrorResponse( "Email already exists" ); + } + 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( "getchecksum" , 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 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 + }; + } + +}; + +module.exports = new ApplicationLogic(); diff --git a/server/src/Apps/Account/Ports/Events/index.js b/server/src/Apps/Account/Ports/Events/index.js new file mode 100644 index 0000000..1cc63f6 --- /dev/null +++ b/server/src/Apps/Account/Ports/Events/index.js @@ -0,0 +1,9 @@ +'use strict'; +const App = require('../../App'); + +/** + * Dictionary of event ids and handlers + */ +module.exports = { + // "event_id" : App.onEvent +}; diff --git a/server/src/Apps/Account/Ports/Interfaces/index.js b/server/src/Apps/Account/Ports/Interfaces/index.js new file mode 100644 index 0000000..f1fa5a8 --- /dev/null +++ b/server/src/Apps/Account/Ports/Interfaces/index.js @@ -0,0 +1,38 @@ +'use strict'; + +const { toSha256 } = require('../../../../Shared/ShaUtils'); + +const { authentication } = require('../../../../../config/apiConfig.json'); + +const { genErrorResponse } = require('../../../../Shared/ErrorResponse'); + +const SharedResources = require('../../../../Shared/Resources'); + +function publishEvent( event , data = null ){ + const EventBus = SharedResources.get("SysS:EventManager"); + const AppEventDomain = "App:Account:" + 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, + publishEvent +}; diff --git a/server/src/Apps/Account/Ports/Public/index.js b/server/src/Apps/Account/Ports/Public/index.js new file mode 100644 index 0000000..c15418c --- /dev/null +++ b/server/src/Apps/Account/Ports/Public/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.export = {}; diff --git a/server/src/Apps/Account/Repository/Objection/index.js b/server/src/Apps/Account/Repository/Objection/index.js new file mode 100644 index 0000000..4704304 --- /dev/null +++ b/server/src/Apps/Account/Repository/Objection/index.js @@ -0,0 +1,76 @@ +'use strict'; + +const { getModel } = require('../../../../Shared/Models/Objection'); +const Model = getModel('users'); +const Sessions = getModel('users_sessions'); +const Companies = getModel('companies'); + +class SpecificModelRepository{ + constructor(){} + + async populateCompany( 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 Model.query() + .select( "*" ) + .where("email","=",email).first(); + } + + async createOne( email, safe_password ){ + const user = await Model.query().insert({ + email, + password : safe_password, + "name":"No name", + "last_name":"No lastname", + "createdAt" : new Date().toISOString() + }); + return this.populateCompany( user ); + } + + async findByEmailPassword( email, safe_password ){ + const user = await Model.query().select('*') + .where("email","=",email) + .where("password","=",safe_password) + .first(); + return this.populateCompany( user ); + } + + 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 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 ); + } + } + +} + +module.exports = new SpecificModelRepository(); \ No newline at end of file diff --git a/server/src/Apps/Account/Repository/index.js b/server/src/Apps/Account/Repository/index.js new file mode 100644 index 0000000..0c45820 --- /dev/null +++ b/server/src/Apps/Account/Repository/index.js @@ -0,0 +1,5 @@ +'use strict'; + +const SpecificModelRepository = require('./Objection'); + +module.exports = SpecificModelRepository; \ No newline at end of file diff --git a/server/src/Controller/index.js b/server/src/Controller/index.js new file mode 100644 index 0000000..d599c3f --- /dev/null +++ b/server/src/Controller/index.js @@ -0,0 +1,8 @@ +const express = require('express'); +const app = express(); + +const account = require('../Apps/Account/Controller'); + +app.use('/account',account); + +module.exports = app; diff --git a/server/src/Shared/ErrorResponse.js b/server/src/Shared/ErrorResponse.js new file mode 100644 index 0000000..0182dbb --- /dev/null +++ b/server/src/Shared/ErrorResponse.js @@ -0,0 +1,12 @@ +'use strict'; + +function genErrorResponse( msg , code = 400 ){ + return { + error : { + code, + msg + } + } +} + +module.exports = { genErrorResponse }; diff --git a/server/src/Shared/Models/Objection/companies.model.js b/server/src/Shared/Models/Objection/companies.model.js new file mode 100644 index 0000000..3571d59 --- /dev/null +++ b/server/src/Shared/Models/Objection/companies.model.js @@ -0,0 +1,24 @@ +'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/server/src/Shared/Models/Objection/company_categories.model.js b/server/src/Shared/Models/Objection/company_categories.model.js new file mode 100644 index 0000000..ade7e4a --- /dev/null +++ b/server/src/Shared/Models/Objection/company_categories.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/company_truck_types.model.js b/server/src/Shared/Models/Objection/company_truck_types.model.js new file mode 100644 index 0000000..7e6b2cf --- /dev/null +++ b/server/src/Shared/Models/Objection/company_truck_types.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/index.js b/server/src/Shared/Models/Objection/index.js new file mode 100644 index 0000000..0115185 --- /dev/null +++ b/server/src/Shared/Models/Objection/index.js @@ -0,0 +1,22 @@ +"use strict"; + +const users = require('./users.model'); +const user_sessions = require('./user_sessions.model'); +const companies = require('./companies.model'); + +function getModel( name ){ + switch( name ){ + case 'users': + return users; + case 'users_sessions': + return user_sessions; + case 'companies': + return companies; + default: + return null; + } +} + +module.exports = { + getModel +}; diff --git a/server/src/Shared/Models/Objection/load_attachments.model.js b/server/src/Shared/Models/Objection/load_attachments.model.js new file mode 100644 index 0000000..30b75b9 --- /dev/null +++ b/server/src/Shared/Models/Objection/load_attachments.model.js @@ -0,0 +1,32 @@ +'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/server/src/Shared/Models/Objection/load_categories.model.js b/server/src/Shared/Models/Objection/load_categories.model.js new file mode 100644 index 0000000..15f92b4 --- /dev/null +++ b/server/src/Shared/Models/Objection/load_categories.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/loads.model.js b/server/src/Shared/Models/Objection/loads.model.js new file mode 100644 index 0000000..3234819 --- /dev/null +++ b/server/src/Shared/Models/Objection/loads.model.js @@ -0,0 +1,60 @@ +'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/server/src/Shared/Models/Objection/location_categories.model.js b/server/src/Shared/Models/Objection/location_categories.model.js new file mode 100644 index 0000000..9e66918 --- /dev/null +++ b/server/src/Shared/Models/Objection/location_categories.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/location_truck_types.model.js b/server/src/Shared/Models/Objection/location_truck_types.model.js new file mode 100644 index 0000000..5d91509 --- /dev/null +++ b/server/src/Shared/Models/Objection/location_truck_types.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/locations.model.js b/server/src/Shared/Models/Objection/locations.model.js new file mode 100644 index 0000000..be0945b --- /dev/null +++ b/server/src/Shared/Models/Objection/locations.model.js @@ -0,0 +1,25 @@ +'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 = Users; diff --git a/server/src/Shared/Models/Objection/metadata_categories.model.js b/server/src/Shared/Models/Objection/metadata_categories.model.js new file mode 100644 index 0000000..23c26c4 --- /dev/null +++ b/server/src/Shared/Models/Objection/metadata_categories.model.js @@ -0,0 +1,18 @@ +'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/server/src/Shared/Models/Objection/metadata_cities.model.js b/server/src/Shared/Models/Objection/metadata_cities.model.js new file mode 100644 index 0000000..5977ab6 --- /dev/null +++ b/server/src/Shared/Models/Objection/metadata_cities.model.js @@ -0,0 +1,21 @@ +'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/server/src/Shared/Models/Objection/metadata_products.model.js b/server/src/Shared/Models/Objection/metadata_products.model.js new file mode 100644 index 0000000..d6d795d --- /dev/null +++ b/server/src/Shared/Models/Objection/metadata_products.model.js @@ -0,0 +1,18 @@ +'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/server/src/Shared/Models/Objection/metadata_truck_types.model.js b/server/src/Shared/Models/Objection/metadata_truck_types.model.js new file mode 100644 index 0000000..2c7f9be --- /dev/null +++ b/server/src/Shared/Models/Objection/metadata_truck_types.model.js @@ -0,0 +1,18 @@ +'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/server/src/Shared/Models/Objection/user_locations.model.js b/server/src/Shared/Models/Objection/user_locations.model.js new file mode 100644 index 0000000..892d83d --- /dev/null +++ b/server/src/Shared/Models/Objection/user_locations.model.js @@ -0,0 +1,36 @@ +'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/server/src/Shared/Models/Objection/user_sessions.model.js b/server/src/Shared/Models/Objection/user_sessions.model.js new file mode 100644 index 0000000..b384971 --- /dev/null +++ b/server/src/Shared/Models/Objection/user_sessions.model.js @@ -0,0 +1,20 @@ +'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/server/src/Shared/Models/Objection/users.model.js b/server/src/Shared/Models/Objection/users.model.js new file mode 100644 index 0000000..ce1772c --- /dev/null +++ b/server/src/Shared/Models/Objection/users.model.js @@ -0,0 +1,36 @@ +'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: "string" }, + is_active : { type : 'boolean', default : true } + } + }; + } +} + +module.exports = Users; diff --git a/server/src/Shared/Models/Objection/vechicle_publications.model.js b/server/src/Shared/Models/Objection/vechicle_publications.model.js new file mode 100644 index 0000000..ce7666d --- /dev/null +++ b/server/src/Shared/Models/Objection/vechicle_publications.model.js @@ -0,0 +1,32 @@ +'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/server/src/Shared/Models/Objection/vehicle_categories.model.js b/server/src/Shared/Models/Objection/vehicle_categories.model.js new file mode 100644 index 0000000..96cc57f --- /dev/null +++ b/server/src/Shared/Models/Objection/vehicle_categories.model.js @@ -0,0 +1,19 @@ +'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/server/src/Shared/Models/Objection/vehicles.model.js b/server/src/Shared/Models/Objection/vehicles.model.js new file mode 100644 index 0000000..ecd1474 --- /dev/null +++ b/server/src/Shared/Models/Objection/vehicles.model.js @@ -0,0 +1,34 @@ +'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/server/src/Shared/Resources/index.js b/server/src/Shared/Resources/index.js new file mode 100644 index 0000000..638a859 --- /dev/null +++ b/server/src/Shared/Resources/index.js @@ -0,0 +1,38 @@ +'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/server/src/Shared/ShaUtils.js b/server/src/Shared/ShaUtils.js new file mode 100644 index 0000000..7804f6b --- /dev/null +++ b/server/src/Shared/ShaUtils.js @@ -0,0 +1,12 @@ +"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/server/src/SysS/Connections/index.js b/server/src/SysS/Connections/index.js new file mode 100644 index 0000000..7820292 --- /dev/null +++ b/server/src/SysS/Connections/index.js @@ -0,0 +1,68 @@ +'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(); + + 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/server/src/SysS/Controller/index.js b/server/src/SysS/Controller/index.js new file mode 100644 index 0000000..139165b --- /dev/null +++ b/server/src/SysS/Controller/index.js @@ -0,0 +1,132 @@ +'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; + + 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; + + app.use( middlewares.errorJSON ); + app.use( AppsController ); + app.use( middlewares.error404 ); + + this.SystemServiceState = INIT; + } + + async connect(){ + const app = this.app; + const serverPort = this.serverPort; + + 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(){ + this.server.close(); + + 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 ExpressJSServices(); diff --git a/server/src/SysS/Controller/middlewares.js b/server/src/SysS/Controller/middlewares.js new file mode 100644 index 0000000..a56b25f --- /dev/null +++ b/server/src/SysS/Controller/middlewares.js @@ -0,0 +1,87 @@ +'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/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js b/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js new file mode 100644 index 0000000..f2296d4 --- /dev/null +++ b/server/src/SysS/EventManager/EmailEvents/SendGrid.handler.js @@ -0,0 +1,84 @@ +'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/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js b/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js new file mode 100644 index 0000000..f48ce78 --- /dev/null +++ b/server/src/SysS/EventManager/EmailEvents/StandAlone.handler.js @@ -0,0 +1,22 @@ +'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/server/src/SysS/EventManager/EmailEvents/index.js b/server/src/SysS/EventManager/EmailEvents/index.js new file mode 100644 index 0000000..12b1765 --- /dev/null +++ b/server/src/SysS/EventManager/EmailEvents/index.js @@ -0,0 +1,36 @@ +'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" : onChecksumGeneration, + "App:Account:signupconfirmed":onAccountConfirmed, + "App:Account:getchecksum:pwdreset":onPasswordReset, + "App:ContactEmail:getchecksum":onContactFromWebPage, +}; diff --git a/server/src/SysS/EventManager/index.js b/server/src/SysS/EventManager/index.js new file mode 100644 index 0000000..76d9152 --- /dev/null +++ b/server/src/SysS/EventManager/index.js @@ -0,0 +1,71 @@ +'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/server/src/SysS/EventManager/resources.js b/server/src/SysS/EventManager/resources.js new file mode 100644 index 0000000..a488db6 --- /dev/null +++ b/server/src/SysS/EventManager/resources.js @@ -0,0 +1,7 @@ +'use strict'; + +const EmailEvents = require('./EmailEvents'); + +module.exports = [ + EmailEvents, +]; \ No newline at end of file diff --git a/server/src/SysS/Template/index.js b/server/src/SysS/Template/index.js new file mode 100644 index 0000000..f3fa6ec --- /dev/null +++ b/server/src/SysS/Template/index.js @@ -0,0 +1,33 @@ +'use strict'; +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(){ + this.SystemServiceState = ONLINE; + } + + async disconnect(){ + this.SystemServiceState = OFFLINE; + } + + async deinit(){ + this.SystemServiceState = UNINIT; + } +} + +module.exports = new SystemServices(); diff --git a/server/src/SysS/index.js b/server/src/SysS/index.js new file mode 100644 index 0000000..4d3904e --- /dev/null +++ b/server/src/SysS/index.js @@ -0,0 +1,78 @@ +'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/server/src/index.js b/server/src/index.js new file mode 100644 index 0000000..f15c00e --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,32 @@ +'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/server/test/index.js b/server/test/index.js new file mode 100644 index 0000000..126d321 --- /dev/null +++ b/server/test/index.js @@ -0,0 +1,18 @@ +"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 ); + }) +}); + diff --git a/src/apps/private/vehicles/services.js b/src/apps/private/vehicles/services.js index 9ae4c94..2e7dda0 100644 --- a/src/apps/private/vehicles/services.js +++ b/src/apps/private/vehicles/services.js @@ -1,168 +1,168 @@ -"use strict"; -const { ROOT_PATH, LIB_PATH, MODELS_PATH, HANDLERS_PATH } = process.env; -const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` ); -const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` ); -const { GenericHandler } = require( `${ROOT_PATH}/${HANDLERS_PATH}/Generic.handler.js` ); -const Model = getModel('vehicles'); - -const populate_list = ['categories', 'active_load','load_shipper','driver']; -const generic = new GenericHandler( Model, null, populate_list ); - -function getAndFilterList( query ){ - const filter_list = []; - const { - categories, - active_load, - load_shipper, - driver, - vehicle_code, - vehicle_name, - vehicle_number, - circulation_serial_number, - truck_type, - tyre_type, - city, - state, - status, - destino - } = query; - - if( categories ) { filter_list.push({ categories }); } - if( active_load ) { filter_list.push({ active_load }); } - if( load_shipper ) { filter_list.push({ load_shipper }); } - if( driver ) { filter_list.push({ driver }); } - if( vehicle_code ) { filter_list.push({ vehicle_code }); } - if( vehicle_name ) { filter_list.push({ vehicle_name }); } - if( vehicle_number ) { filter_list.push({ vehicle_number }); } - if( circulation_serial_number ) { filter_list.push({ circulation_serial_number }); } - if( truck_type ) { filter_list.push({ truck_type }); } - if( tyre_type ) { filter_list.push({ tyre_type }); } - if( city ) { filter_list.push({ city }); } - if( state ) { filter_list.push({ state }); } - if( status ) { filter_list.push({ status }); } - if( destino ) { filter_list.push({ destino }); } - - if( filter_list.length == 0 ){ - return null; - } - return filter_list; -} - -async function findElements( companyId , query ){ - const { page, elements } = getPagination( query ); - const andFilterList = getAndFilterList( query ); - let filter; - if( andFilterList ){ - andFilterList.push({ company : companyId }); - filter = { $and : andFilterList }; - }else{ - filter = { company : companyId }; - } - const { total , limit, skip, data } = await generic.getList( page , elements, filter ); - return { - total, - limit, - skip, - data:data - }; -} - -async function findElementById( elementId , companyId ){ - let retVal = await Model.findById( elementId ).populate( populate_list ) || {}; - return retVal; -} - -const findList = async(req, res) => { - try{ - const query = req.query || {}; - const companyId = req.context.companyId; - const retVal = await findElements( companyId , query ); - res.send( retVal ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -}; - -const getById = async(req, res) => { - try{ - const companyId = req.context.companyId; - const elementId = req.params.id; - res.send( await findElementById( elementId , companyId ) ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -}; - -const patchVehicle = async(req, res) => { - try{ - const companyId = req.context.companyId; - const elementId = req.params.id; - const permissions = req.context.permissions; - const vehicle = await findElementById( elementId , companyId ); - const data = req.body; - if( !vehicle ){ - throw "You can't modify this vehicle"; - } - if( !data ){ - throw "Vehicle data not sent"; - } - if( permissions !== "role_carrier" ){ - throw "You can't modify vehicles"; - } - data.company = companyId; - await Model.findByIdAndUpdate( elementId , data ); - return res.send( await Model.findById( elementId ) ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -}; - -const postVehicle = async(req, res) => { - try{ - const userId = req.context.userId; - const companyId = req.context.companyId; - const permissions = req.context.permissions; - const data = req.body; - if( !data ){ - throw "Vehicle data not sent"; - } - if(permissions !== "role_carrier" ){ - throw "You can't create vehicles"; - } - data.company = companyId; - data.status = "Free"; - data.is_available = false; - data.posted_by = userId; - const vehicle = new Model( data ); - await vehicle.save(); - return res.send( vehicle ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -}; - -const deleteVehicle = async(req, res) => { - try{ - const companyId = req.context.companyId; - const elementId = req.params.id; - const permissions = req.context.permissions; - const vehicle = await findElementById( elementId , companyId ); - if( !vehicle ){ - throw "You can't delete this vehicle"; - } - if(permissions !== "role_carrier" ){ - throw "You can't delete vehicles"; - } - await Model.findByIdAndDelete( elementId ); - return res.send(vehicle); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -}; - -module.exports = { findList, getById, patchVehicle, postVehicle, deleteVehicle }; +"use strict"; +const { ROOT_PATH, LIB_PATH, MODELS_PATH, HANDLERS_PATH } = process.env; +const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` ); +const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` ); +const { GenericHandler } = require( `${ROOT_PATH}/${HANDLERS_PATH}/Generic.handler.js` ); +const Model = getModel('vehicles'); + +const populate_list = ['categories', 'active_load','load_shipper','driver']; +const generic = new GenericHandler( Model, null, populate_list ); + +function getAndFilterList( query ){ + const filter_list = []; + const { + categories, + active_load, + load_shipper, + driver, + vehicle_code, + vehicle_name, + vehicle_number, + circulation_serial_number, + truck_type, + tyre_type, + city, + state, + status, + destino + } = query; + + if( categories ) { filter_list.push({ categories }); } + if( active_load ) { filter_list.push({ active_load }); } + if( load_shipper ) { filter_list.push({ load_shipper }); } + if( driver ) { filter_list.push({ driver }); } + if( vehicle_code ) { filter_list.push({ vehicle_code }); } + if( vehicle_name ) { filter_list.push({ vehicle_name }); } + if( vehicle_number ) { filter_list.push({ vehicle_number }); } + if( circulation_serial_number ) { filter_list.push({ circulation_serial_number }); } + if( truck_type ) { filter_list.push({ truck_type }); } + if( tyre_type ) { filter_list.push({ tyre_type }); } + if( city ) { filter_list.push({ city }); } + if( state ) { filter_list.push({ state }); } + if( status ) { filter_list.push({ status }); } + if( destino ) { filter_list.push({ destino }); } + + if( filter_list.length == 0 ){ + return null; + } + return filter_list; +} + +async function findElements( companyId , query ){ + const { page, elements } = getPagination( query ); + const andFilterList = getAndFilterList( query ); + let filter; + if( andFilterList ){ + andFilterList.push({ company : companyId }); + filter = { $and : andFilterList }; + }else{ + filter = { company : companyId }; + } + const { total , limit, skip, data } = await generic.getList( page , elements, filter ); + return { + total, + limit, + skip, + data:data + }; +} + +async function findElementById( elementId , companyId ){ + let retVal = await Model.findById( elementId ).populate( populate_list ) || {}; + return retVal; +} + +const findList = async(req, res) => { + try{ + const query = req.query || {}; + const companyId = req.context.companyId; + const retVal = await findElements( companyId , query ); + res.send( retVal ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +}; + +const getById = async(req, res) => { + try{ + const companyId = req.context.companyId; + const elementId = req.params.id; + res.send( await findElementById( elementId , companyId ) ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +}; + +const patchVehicle = async(req, res) => { + try{ + const companyId = req.context.companyId; + const elementId = req.params.id; + const permissions = req.context.permissions; + const vehicle = await findElementById( elementId , companyId ); + const data = req.body; + if( !vehicle ){ + throw "You can't modify this vehicle"; + } + if( !data ){ + throw "Vehicle data not sent"; + } + if( permissions !== "role_carrier" ){ + throw "You can't modify vehicles"; + } + data.company = companyId; + await Model.findByIdAndUpdate( elementId , data ); + return res.send( await Model.findById( elementId ) ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +}; + +const postVehicle = async(req, res) => { + try{ + const userId = req.context.userId; + const companyId = req.context.companyId; + const permissions = req.context.permissions; + const data = req.body; + if( !data ){ + throw "Vehicle data not sent"; + } + if(permissions !== "role_carrier" ){ + throw "You can't create vehicles"; + } + data.company = companyId; + data.status = "Free"; + data.is_available = false; + data.posted_by = userId; + const vehicle = new Model( data ); + await vehicle.save(); + return res.send( vehicle ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +}; + +const deleteVehicle = async(req, res) => { + try{ + const companyId = req.context.companyId; + const elementId = req.params.id; + const permissions = req.context.permissions; + const vehicle = await findElementById( elementId , companyId ); + if( !vehicle ){ + throw "You can't delete this vehicle"; + } + if(permissions !== "role_carrier" ){ + throw "You can't delete vehicles"; + } + await Model.findByIdAndDelete( elementId ); + return res.send(vehicle); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +}; + +module.exports = { findList, getById, patchVehicle, postVehicle, deleteVehicle };