Merge pull request #3 from flotte-goes-smart/feature/userserver-tcp-connection

Feature/userserver tcp connection
pull/4/head
leonnicolas 4 years ago committed by GitHub
commit 623fd9e593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -24,8 +24,9 @@
"max-classes-per-file": 0,
"no-var-requires": 0,
"array-type": 0,
"semi": 1,
"semi-style": ["error", "last"]
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"no-extra-semi": "off"
}
}

2
.gitignore vendored

@ -1,2 +1,4 @@
node_modules
dist
.env
.idea

61
package-lock.json generated

@ -306,6 +306,15 @@
"@types/express": "*"
}
},
"@types/crc": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/crc/-/crc-3.4.0.tgz",
"integrity": "sha1-I2a+tDmc1zSzPkLHrICVduYX1Io=",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.8",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz",
@ -2071,6 +2080,14 @@
"vary": "^1"
}
},
"crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"requires": {
"buffer": "^5.1.0"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -4958,6 +4975,11 @@
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
"messagepack": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/messagepack/-/messagepack-1.1.12.tgz",
"integrity": "sha512-pNB6K4q4VMLRXdvlGZkTtQhmKFntvLisnOQnL0VhKpZooL8B8Wsv5TXuidIJil0bCH6V172p3+Onfyow0usPYQ=="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -5893,6 +5915,45 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"promise-duplex": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/promise-duplex/-/promise-duplex-6.0.0.tgz",
"integrity": "sha512-ZL7rquzjTFzInDBeWYcsT+qddolNvzigahk6MI6qLSbQvlyRRCJkU3JztgaVunzvkH28smRa2Qu/cY9RXtSkgA==",
"requires": {
"core-js": "^3.6.5",
"promise-readable": "^6.0.0",
"promise-writable": "^6.0.0"
}
},
"promise-readable": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/promise-readable/-/promise-readable-6.0.0.tgz",
"integrity": "sha512-5NxtmUswijvX5cAM0zPSy6yiCXH/eKBpiiBq6JfAUrmngMquMbzcBhF2qA+ocs4rYYKdvAfv3cOvZxADLtL1CA==",
"requires": {
"core-js": "^3.6.5"
}
},
"promise-socket": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/promise-socket/-/promise-socket-7.0.0.tgz",
"integrity": "sha512-Oic9BrxmcHOPEnzKp2Js+ehFyvsbd0WxsE5khweCTHuRvdzbXjHUZmSDT6F9TW8SIkAJ0lCzoHjMYnb0WQJPiw==",
"requires": {
"promise-duplex": "^6.0.0",
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"promise-writable": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/promise-writable/-/promise-writable-6.0.0.tgz",
"integrity": "sha512-b81zre/itgJFS7dwWzIdKNVVqvLiUxYRS/wolUB0H1YY/tAaS146XGKa4Q/5wCbsnXLyn0MCeV6f8HHe4iUHLg=="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",

@ -27,12 +27,19 @@
"gulp-eslint": "^6.0.0",
"gulp-nodemon": "^2.5.0",
"gulp-typescript": "^6.0.0-alpha.1",
"typescript": "^4.0.2"
"typescript": "^4.0.2",
"@types/crc": "^3.4.0"
},
"dependencies": {
"apollo-server": "^2.17.0",
"apollo-server-express": "^2.17.0",
"crc": "^3.8.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"graphql": "^15.3.0",
"messagepack": "^1.1.12",
"pg": "^8.3.3",
"promise-socket": "^7.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.26"
}

@ -1,14 +1,14 @@
import { DataSource } from 'apollo-datasource'
import { getConnection, Connection } from 'typeorm'
import { CargoBike } from '../../model/CargoBike'
import { DataSource } from 'apollo-datasource';
import { getConnection, Connection } from 'typeorm';
import { CargoBike } from '../../model/CargoBike';
/**
* extended datasource to feed resolvers with data about cargoBikes
*/
export class CargoBikeAPI extends DataSource {
connection : Connection
constructor () {
super()
this.connection = getConnection()
super();
this.connection = getConnection();
}
/**
@ -18,15 +18,15 @@ export class CargoBikeAPI extends DataSource {
return {
id,
name: token
}
};
}
async updateBike ({ id, token, name }:{id:any, token: string, name: string }) {
const bike = new CargoBike()
bike.id = id
bike.description = token
bike.name = name
this.connection.manager.save(bike)
const bike = new CargoBike();
bike.id = id;
bike.description = token;
bike.name = name;
await this.connection.manager.save(bike);
return {
success: true,
message: token,
@ -34,6 +34,6 @@ export class CargoBikeAPI extends DataSource {
id,
name
}
}
};
}
}

@ -0,0 +1,43 @@
import { encode, decode } from 'messagepack';
import { Method } from './method';
import { crc32 } from 'crc';
export class RPCMessage<T> {
private readonly method: Method
readonly data: T
constructor (method: Method, data: any) {
this.method = method;
this.data = data;
}
static fromBuffer<T> (raw: Buffer): RPCMessage<T> {
const length = raw.readUInt32BE();
if (raw.length !== length) {
throw new Error('Invalid Buffer length');
}
const crcNum = raw.readUInt32BE(length - 4);
if (crc32(raw.slice(0, length - 4)) !== crcNum) {
throw new Error('Validation check failed');
}
const method = raw.readUInt32BE(4);
const msgData = decode(raw.slice(8, length));
return new RPCMessage(method, msgData);
}
public toBuffer (): Buffer {
const msgData = encode(this.data);
const length = msgData.length + 12;
const buffer = Buffer.alloc(length - 4);
buffer.writeUInt32BE(length);
buffer.writeUInt32BE(this.method, 4);
buffer.fill(msgData, 8);
const crcNum = crc32(buffer);
const resultBuffer = Buffer.alloc(length, buffer);
resultBuffer.writeUInt32BE(crcNum, length - 4);
return resultBuffer;
}
}

@ -0,0 +1,11 @@
/* eslint no-unused-vars: 0 */
export enum Method {
Null = 0x00,
Error = 0x0F_0F_0F_0F,
Info = 0x49_4e_46_4f,
ValidateToken = 0x56_41_4c_49,
GetRoles = 0x52_4f_4c_45,
GetRolePermissions = 0x50_45_52_4d,
CreateRole = 0x43_52_4f_4c,
CreatePermissions = 0x43_50_45_52
}

@ -0,0 +1,17 @@
/* eslint no-unused-vars: 0 */
export enum Permission {
ReadBike = 'BIKE_READ',
WriteBike = 'BIKE_WRITE',
}
// Permissions where the creation will be requested on startup
export const requiredPermissions = [
{
name: Permission.ReadBike,
description: 'Allows to read of bike information'
},
{
name: Permission.WriteBike,
description: 'Allows the modification of bike information'
}
];

@ -0,0 +1,13 @@
// Response in the form of [valid, ttl]
export type ValidateTokenResponse = [boolean, number]
// Info in the form of [name, binary, description, request_structure][]
export type GetInfoResponse = [string, string, string][]
// Role array in the form of [id, name, description][]
export type GetRolesResponse = [number, string, string][]
// Permissions map where each roleId maps to an array of permissions
export type GetRolesPermissionsResponse = {[id: string]: [number, string, string][]}
export type CreateRoleResponse = [number, string, string]

@ -1,13 +1,127 @@
import { DataSource } from 'apollo-datasource'
import { DataSource } from 'apollo-datasource';
import { Socket } from 'net';
import { PromiseSocket } from 'promise-socket';
import { RPCMessage } from './message';
import { Method } from './method';
import {
CreateRoleResponse,
GetInfoResponse,
GetRolesPermissionsResponse,
GetRolesResponse,
ValidateTokenResponse
} from './responses';
import { requiredPermissions } from './permission';
/**
* fetches datafrom user server, especially validates user tokens
*/
export class UserServerAPI extends DataSource {
port: number
host: string
constructor (address: string) {
super();
const parts = address.split(':');
this.host = parts[0];
if (parts.length === 2) {
this.port = Number(parts[1]);
} else {
this.port = 5000;
}
}
/**
* Returns the information about all available rpc methods
*/
async getInfo (): Promise<GetInfoResponse> {
const response = await this.send<GetInfoResponse>(new RPCMessage(Method.Info, null));
return response.data;
}
/**
* Creates required API permissions
*/
async createDefinedPermissions () {
await this.send<any>(new RPCMessage(Method.CreatePermissions, { permissions: requiredPermissions }));
}
/**
* Creates a new role with the given parameters
* @param name - the name of the role
* @param description - a description of the role
* @param permissionIDs - an array of IDs the role is created with
*/
async createRole (name: string, description: string, permissionIDs: number[]): Promise<CreateRoleResponse> {
const response = await this.send<CreateRoleResponse>(new RPCMessage<any>(Method.CreateRole, {
name,
description,
permissions: permissionIDs
}));
return response.data;
}
/**
* validates user token
*/
async validateToken (token:string) {
return true
async validateToken (token:string): Promise<boolean> {
const response = await this.send<ValidateTokenResponse>(new RPCMessage(Method.ValidateToken, { token }));
if (response) {
return response.data[0];
} else {
return false;
}
}
/**
* Returns a list of roles the user is assigned to
* @param token
*/
async getUserRoles (token: String): Promise<GetRolesResponse> {
const response = await this.send<any>(new RPCMessage(Method.GetRoles, { token }));
return response.data;
}
/**
* Returns all permissions of the user
* @param token
*/
async getUserPermissions (token: String): Promise<string[]> {
const roles = await this.getUserRoles(token);
const roleIds = roles.map(role => role[0]);
const permissionsResponse = await this.send<GetRolesPermissionsResponse>(new RPCMessage(Method.GetRolePermissions, { roles: roleIds }));
const permissions: string[] = [];
for (const id of roleIds) {
permissions.push(...permissionsResponse.data[id].map(entry => entry[1]));
}
return permissions;
}
/**
* Connects to the socket and returns the client
*/
async getSocket (): Promise<PromiseSocket<Socket>> {
const socket = new Socket();
const promiseSocket = new PromiseSocket(socket);
await promiseSocket.connect(this.port, this.host);
return promiseSocket;
}
/**
* Sends a message and reads and parses the response of it
* @param message
*/
async send<T> (message: RPCMessage<any>): Promise<RPCMessage<T>> {
const socket = await this.getSocket();
await socket.writeAll(message.toBuffer());
const response = await socket.readAll() as Buffer;
if (response?.length > 0) {
return RPCMessage.fromBuffer<T>(response);
}
}
}

@ -1,22 +1,68 @@
import { ApolloServer } from 'apollo-server'
import bikeresolver from './resolvers/cargobikeResolver'
import { CargoBikeAPI } from './datasources/db/cargobikeAPI'
import typeDefs from './schema/type-defs'
import 'reflect-metadata'
import { createConnection } from 'typeorm'
import { ApolloServer } from 'apollo-server-express';
import bikeresolver from './resolvers/cargobikeResolver';
import { CargoBikeAPI } from './datasources/db/cargobikeAPI';
import typeDefs from './schema/type-defs';
import 'reflect-metadata';
import { createConnection } from 'typeorm';
import { UserServerAPI } from './datasources/userserver/userserviceAPI';
import express from 'express';
import { requiredPermissions } from './datasources/userserver/permission';
require('dotenv').config();
/**
* Function that is called to authenticate a user by using the user rpc server
* @param req
* @param res
* @param next
*/
async function authenticate (req: any, res: any, next: any) {
if (process.env.NODE_ENV === 'develop') {
req.permissions = requiredPermissions.map((e) => e.name);
next();
} else {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
if (await userAPI.validateToken(token)) {
req.permissions = await userAPI.getUserPermissions(token);
next();
} else {
res.status(401);
res.send('Unauthorized');
}
} else {
res.status(401);
res.send('Unauthorized');
}
}
}
createConnection().then(async () => {
console.log('connected to db')
}).catch(error => console.log(error))
console.log('connected to db');
}).catch(error => console.log(error));
const userAPI = new UserServerAPI(process.env.RPC_HOST);
const server = new ApolloServer({
resolvers: [bikeresolver],
typeDefs,
dataSources: () => ({
cargoBikeAPI: new CargoBikeAPI()
})
})
cargoBikeAPI: new CargoBikeAPI(),
userAPI
}),
context: (req: any) => {
return req;
}
});
const app = express();
app.post('/graphql', authenticate);
app.get(/\/graphql?&.*query=/, authenticate);
server.applyMiddleware({ app });
console.log(__dirname)
server.listen()
.then(({ url }) => console.log(`Server ready at ${url} `))
console.log(__dirname);
app.listen(4000, async () => {
console.log('Server listening on port 4000');
await userAPI.createDefinedPermissions();
});

@ -1,4 +1,4 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class CargoBike {

@ -1,10 +1,23 @@
import { Permission } from '../datasources/userserver/permission';
import { GraphQLError } from 'graphql';
export default {
Query: {
CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources }:{dataSources: any}) =>
dataSources.cargoBikeAPI.findCargoBikeById({ id, token })
CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources, req }:{dataSources: any, req: any }) => {
if (req.permissions.includes(Permission.ReadBike)) {
return dataSources.cargoBikeAPI.findCargoBikeById({ id, token });
} else {
throw new GraphQLError('Insufficient Permissions');
}
}
},
Mutation: {
addBike: (_:any, { id, token, name }:{id: any, token: string, name:string}, { dataSources }:{dataSources: any }) =>
dataSources.cargoBikeAPI.updateBike({ id, token, name })
addBike: (_: any, { id, token, name }:{id: any, token: string, name:string}, { dataSources, req }:{dataSources: any, req: any }) => {
if (req.permissions.includes(Permission.WriteBike)) {
return dataSources.cargoBikeAPI.updateBike({ id, token, name });
} else {
throw new GraphQLError('Insufficient Permissions');
}
}
}
}
};

@ -1,4 +1,4 @@
import { gql } from 'apollo-server'
import { gql } from 'apollo-server';
export default gql`
@ -355,4 +355,4 @@ type Mutation {
cargoBike(token:String!,cargoBike: CargoBikeInput): UpdateBikeResponse!
}
`
`;

Loading…
Cancel
Save