diff --git a/v2/package.json b/v2/package.json index 5305286..e4b9c54 100644 --- a/v2/package.json +++ b/v2/package.json @@ -27,10 +27,13 @@ "cors": "^2.8.5", "crypto-js": "^4.1.1", "dotenv": "^16.3.1", - "express": "^4.18.2", + "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", diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js b/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js new file mode 100644 index 0000000..19e7838 --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Controller/graphql/resolvers.js @@ -0,0 +1,31 @@ +'use strict'; +const { DateResolver, DateTimeResolver } = require('graphql-scalars'); +const { Account, User, Company } = require('../../Domain'); + +////////////////////////////////////////////// +// Queries +////////////////////////////////////////////// +async function account( args, context ) { + const account = new Account( context.graphQLContext.userId ); + return account; +} + +async function profile( args, context ) { + const profile = new User( context.graphQLContext.userId ); + return profile; +} + +async function company( args, context ) { + const company = new Company( context.graphQLContext.companyId ); + return company; +} + +////////////////////////////////////////////// +// Mutations +////////////////////////////////////////////// + +module.exports = { + account, + profile, + company +}; diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql new file mode 100644 index 0000000..efd212a --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.graphql @@ -0,0 +1,88 @@ +type Query { + account : Account! + profile : User! + company : Company! +} + +scalar DateTime + +type Session { + token : String! + expiration : DateTime! +} + +type Account { + user : User! + + sessions( limit: Int , offset : Int ) : [Session]! + sessionsCount : Int! +} + +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]! + categoriesCount : Int! +} + +type TruckType{ + id : Int! + category : String! +} + +type CompanyCategory{ + id : Int! + category : String! +} + +type Company { + id : Int! + owner : User! + type : String! + is_hidden : Boolean! + is_active : Boolean! + + name : String! + description : String + + createdAt : DateTime! + + locations( limit: Int , offset : Int ) : [Location]! + locationsCount : Int! + + categories : [LocationCategory]! + categoriesCount : Int! + + truck_types : [TruckType]! + truck_typesCount : Int! +} + +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]! + locationsCount : Int! +} diff --git a/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js new file mode 100644 index 0000000..c528542 --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Controller/graphql/schema.js @@ -0,0 +1,8 @@ +'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/index.js b/v2/server/src/Apps/PrivateResources/Controller/index.js new file mode 100644 index 0000000..564a0c1 --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Controller/index.js @@ -0,0 +1,26 @@ +'use strict'; +const router = require('express').Router(); + +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'); + +router.get('/test', async (req, res) => { + console.log( req.graphQLContext ); + res.status(200).send({ + msg : "It is alive!" + }); +} ); + +router.post( '/graphql', + createHandler({ + schema: schemaDescription, + rootValue : schemaResolvers, + context: async (req, params) => { return { graphQLContext : req.raw.graphQLContext }; }, + graphiql: true + }) +); + +module.exports = router; diff --git a/v2/server/src/Apps/PrivateResources/Domain/index.js b/v2/server/src/Apps/PrivateResources/Domain/index.js new file mode 100644 index 0000000..98cb21d --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Domain/index.js @@ -0,0 +1,179 @@ +'use strict'; + +const Repository = require('../Repository'); + +class Company { + constructor( companyId , owner = null ){ + this._companyId = companyId; + this._company = null; + this._owner = owner; + } + + 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(); + } + } + + async id(){ + return this._companyId; + } + async owner(){ + await this.populate_content(); + return this._owner; + } + + 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; + } + + // locations( limit: Int , offset : Int ) : [Location]! + // locationsCount : Int! + // categories : [LocationCategory]! + // categoriesCount : Int! + // truck_types : [TruckType]! + // truck_typesCount : Int! +} + +class User { + constructor( userId, company = null ){ + this._userId = userId; + this._user = null; + + this._company = company; + } + + 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(); + } + } + + 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(){ + return []; + } + + async locationsCount(){ + return 0; + } +} + +class Account { + constructor( userId ){ + this._userId = userId; + this._sessions = null; + this._user = null; + } + + async user(){ + if( this._user ){ + return this._user; + } + + this._user = new User( this._userId ); + return this._user; + } + + async _update_sessions(){ + if( this._sessions ){ + return; + } + + this._sessions = (await Repository.getSessions( this._userId )).map( (item) => { + return { + token : item.token, + expiration : item.expiration + }; + } ); + } + + async sessions(){ + await this._update_sessions(); + return this._sessions; + } + + async sessionsCount(){ + await this._update_sessions(); + return this._sessions.length; + } + +}; + +module.exports = { + Account, + User, + Company +}; diff --git a/v2/server/src/Apps/PrivateResources/Ports/Events/index.js b/v2/server/src/Apps/PrivateResources/Ports/Events/index.js new file mode 100644 index 0000000..110976d --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Ports/Events/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = {}; diff --git a/v2/server/src/Apps/PrivateResources/Ports/Interfaces/index.js b/v2/server/src/Apps/PrivateResources/Ports/Interfaces/index.js new file mode 100644 index 0000000..110976d --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Ports/Interfaces/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = {}; diff --git a/v2/server/src/Apps/PrivateResources/Ports/Public/index.js b/v2/server/src/Apps/PrivateResources/Ports/Public/index.js new file mode 100644 index 0000000..110976d --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Ports/Public/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = {}; diff --git a/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js b/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js new file mode 100644 index 0000000..4b08003 --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/Repository/Objection/index.js @@ -0,0 +1,25 @@ +'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 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); + } + +} + +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 new file mode 100644 index 0000000..0c45820 --- /dev/null +++ b/v2/server/src/Apps/PrivateResources/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/v2/server/src/Controller/index.js b/v2/server/src/Controller/index.js index d599c3f..efe5aca 100644 --- a/v2/server/src/Controller/index.js +++ b/v2/server/src/Controller/index.js @@ -1,8 +1,16 @@ const express = require('express'); const app = express(); -const account = require('../Apps/Account/Controller'); +const middlewares = require('./middlewares'); -app.use('/account',account); +const account = require('../Apps/Account/Controller'); +const privateResources = require('../Apps/PrivateResources/Controller'); + +app.use('/account', account); + +app.use( middlewares.jwtValidator ); +app.use( middlewares.contextGenerator ); + +app.use('/private', privateResources); module.exports = app; diff --git a/v2/server/src/Controller/middlewares.js b/v2/server/src/Controller/middlewares.js new file mode 100644 index 0000000..dea44ed --- /dev/null +++ b/v2/server/src/Controller/middlewares.js @@ -0,0 +1,54 @@ +'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{ + return res.status(401).send({error:"Unauthorized",code:401}); + } +} + +async function contextGenerator( req, res, next ){ + if( ! req.JWT?.isValid ){ + return res.status(401).send({error:"Unauthorized",code:401}); + } + 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.graphQLContext = { + userId, + companyId, + job_role, + permissions, + user + } + + return next(); +} + +module.exports = { + jwtValidator, + contextGenerator +};