Merge pull request #3 from flotte-goes-smart/feature/userserver-tcp-connection
Feature/userserver tcp connectionpull/4/head
commit
623fd9e593
@ -1,2 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
.env
|
||||||
|
.idea
|
||||||
|
@ -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
|
* fetches datafrom user server, especially validates user tokens
|
||||||
*/
|
*/
|
||||||
export class UserServerAPI extends DataSource {
|
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
|
* validates user token
|
||||||
*/
|
*/
|
||||||
async validateToken (token:string) {
|
async validateToken (token:string): Promise<boolean> {
|
||||||
return true
|
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 { ApolloServer } from 'apollo-server-express';
|
||||||
import bikeresolver from './resolvers/cargobikeResolver'
|
import bikeresolver from './resolvers/cargobikeResolver';
|
||||||
import { CargoBikeAPI } from './datasources/db/cargobikeAPI'
|
import { CargoBikeAPI } from './datasources/db/cargobikeAPI';
|
||||||
import typeDefs from './schema/type-defs'
|
import typeDefs from './schema/type-defs';
|
||||||
import 'reflect-metadata'
|
import 'reflect-metadata';
|
||||||
import { createConnection } from 'typeorm'
|
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 () => {
|
createConnection().then(async () => {
|
||||||
console.log('connected to db')
|
console.log('connected to db');
|
||||||
}).catch(error => console.log(error))
|
}).catch(error => console.log(error));
|
||||||
|
|
||||||
|
const userAPI = new UserServerAPI(process.env.RPC_HOST);
|
||||||
|
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
resolvers: [bikeresolver],
|
resolvers: [bikeresolver],
|
||||||
typeDefs,
|
typeDefs,
|
||||||
dataSources: () => ({
|
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)
|
console.log(__dirname);
|
||||||
server.listen()
|
app.listen(4000, async () => {
|
||||||
.then(({ url }) => console.log(`Server ready at ${url} `))
|
console.log('Server listening on port 4000');
|
||||||
|
await userAPI.createDefinedPermissions();
|
||||||
|
});
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
|
import { Permission } from '../datasources/userserver/permission';
|
||||||
|
import { GraphQLError } from 'graphql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources }:{dataSources: any}) =>
|
CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources, req }:{dataSources: any, req: any }) => {
|
||||||
dataSources.cargoBikeAPI.findCargoBikeById({ id, token })
|
if (req.permissions.includes(Permission.ReadBike)) {
|
||||||
|
return dataSources.cargoBikeAPI.findCargoBikeById({ id, token });
|
||||||
|
} else {
|
||||||
|
throw new GraphQLError('Insufficient Permissions');
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
addBike: (_:any, { id, token, name }:{id: any, token: string, name:string}, { dataSources }:{dataSources: any }) =>
|
addBike: (_: any, { id, token, name }:{id: any, token: string, name:string}, { dataSources, req }:{dataSources: any, req: any }) => {
|
||||||
dataSources.cargoBikeAPI.updateBike({ id, token, name })
|
if (req.permissions.includes(Permission.WriteBike)) {
|
||||||
|
return dataSources.cargoBikeAPI.updateBike({ id, token, name });
|
||||||
|
} else {
|
||||||
|
throw new GraphQLError('Insufficient Permissions');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
Loading…
Reference in New Issue