diff --git a/client/package-lock.json b/client/package-lock.json index 2f4b4a7ba..7ada4ee5a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "@superalgos/operations-client", - "version": "0.0.2", + "version": "0.0.13", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/client/package.json b/client/package.json index 43a89253f..40b5a1d21 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@superalgos/operations-client", - "version": "0.0.13", + "version": "0.0.14", "description": "Superalgos Operations client-side web app", "main": "src/index.js", "dependencies": { diff --git a/client/src/GraphQL/Calls/index.js b/client/src/GraphQL/Calls/index.js index f410adf89..7f1d17294 100644 --- a/client/src/GraphQL/Calls/index.js +++ b/client/src/GraphQL/Calls/index.js @@ -1,7 +1,9 @@ import clones from './clones'; import teams from './teams'; +import keys from './keys'; export { clones, - teams + teams, + keys }; diff --git a/client/src/GraphQL/Calls/keys.js b/client/src/GraphQL/Calls/keys.js new file mode 100644 index 000000000..97bd6b26f --- /dev/null +++ b/client/src/GraphQL/Calls/keys.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag'; + +export const GET_ALL_KEYS = gql` + query { + keyVault_AvailableKeys { + id + key + description + } + } +` + +export default { + GET_ALL_KEYS +}; diff --git a/client/src/GraphQL/Fragments/clone.js b/client/src/GraphQL/Fragments/clone.js index 42022c0bd..6993b966b 100644 --- a/client/src/GraphQL/Fragments/clone.js +++ b/client/src/GraphQL/Fragments/clone.js @@ -30,5 +30,6 @@ export const clone = gql` botAvatar teamAvatar processName + keyId } `; diff --git a/client/src/GraphQL/models.js b/client/src/GraphQL/models.js index c7a5df51b..82f6c331e 100644 --- a/client/src/GraphQL/models.js +++ b/client/src/GraphQL/models.js @@ -3,7 +3,6 @@ const availableMonths = ["January", "February", "March", "April", "May", "June", const tradingStartModes ={ live: "Live", - competition: "Competition", backtest: "Backtest" } diff --git a/client/src/index.js b/client/src/index.js index 970c534aa..fbec00c0c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -12,10 +12,10 @@ import Home from './views/Home' const App = () => ( - - - - + + + + ) diff --git a/client/src/views/Clone/Add/index.js b/client/src/views/Clone/Add/index.js index 3db9ccfdd..15bda776f 100644 --- a/client/src/views/Clone/Add/index.js +++ b/client/src/views/Clone/Add/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import { graphql, compose, Query } from 'react-apollo' -import { clones, teams } from '../../../GraphQL/Calls' +import { clones, teams, keys } from '../../../GraphQL/Calls' import { DateTime } from 'luxon'; import TopBar from '../../BannerTopBar' @@ -45,6 +45,7 @@ class AddClone extends Component { runAsTeam: false, teams: [], teamId: '', + keyId: '', // Indicator Bot startYear: 2019, @@ -60,7 +61,8 @@ class AddClone extends Component { isNewCloneConfirmationOpen: false, serverResponse: '', serverError: false, - processNameError: false + processNameError: false, + keyIdError: false } } @@ -230,6 +232,60 @@ class AddClone extends Component { } + { this.state.mode == "live" && + + + Select one of your available Exchange API Keys to be used by this clone. + + + + {({ loading, error, data }) => { + if (loading) return ( + this.setState({keyId:e.target.value})} + onBlur={(e)=>this.setState({keyIdError:false})} + error={this.state.keyIdError} + fullWidth + > + + 'Loading keys...' + + + ); + if (error) return `Error! ${error.message}`; + const list = data.keyVault_AvailableKeys.map((key) => ( + + {key.key} + + )); + return ( + this.setState({keyId:e.target.value})} + onBlur={(e)=>this.setState({keyIdError:false})} + error={this.state.keyIdError} + fullWidth + > + { list } + + ); + }} + + + + + + } + If Run as Team is selected, the clone will be taken from the team respository instead of your own team member folder. @@ -399,7 +455,8 @@ class AddClone extends Component { - @@ -431,7 +488,7 @@ class AddClone extends Component { let serverResponse = await this.createCloneOnServer() error = serverResponse.errors || !isDefined(serverResponse.data.operations_AddClone) if(error){ - this.state.serverResponse = serverResponse.errors[0].message + this.state.serverResponse = serverResponse.errors[0].message || serverResponse.errors[0] this.state.serverError = true }else{ this.state.serverResponse = "The new clone was sucessfully created." @@ -453,15 +510,16 @@ class AddClone extends Component { } } - if(this.state.mode === "backtest"){ - variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0 - variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0 - variables.clone.waitTime = this.state.waitTime - } - if(this.state.selectedBot.kind === "TRADER"){ variables.clone.runAsTeam = this.state.runAsTeam variables.clone.processName = 'Trading-Process' + if(this.state.mode === "backtest"){ + variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0 + variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0 + variables.clone.waitTime = this.state.waitTime + } else { + variables.clone.keyId = this.state.keyId + } } else { variables.clone.processName = this.state.processName variables.clone.startYear = this.state.startYear @@ -508,6 +566,7 @@ class AddClone extends Component { processName: '', teams: [], teamId: '', + keyId: '', //Error handlers nameError: false, @@ -517,7 +576,8 @@ class AddClone extends Component { isNewCloneConfirmationOpen: false, serverResponse: '', serverError: false, - processNameError: false + processNameError: false, + keyIdError: false }) }; diff --git a/server/README.md b/server/README.md index bb4cbecf6..6607a6cfd 100644 --- a/server/README.md +++ b/server/README.md @@ -2,3 +2,32 @@ ## AA Operations Server Server side functions support the business rules for the module. + +## Available Methods + +# Active clones List +- List active personal clones +- List active team clones (future release) + +# History clone List +- List history personal clones +- List history team clones (future release) + +# Get clone by id +- Get existing clones corresponding to the clone ids passed by parameter and returns: + - Clone id + - Team id + - Bot id + +# Add new clones: + Receive a list of clones, create each of them and return the newly created clone. + A clone can be created: + - As a personal clone (run from the personal user folder) + - As a team clone (on competitions, run from the team folder) + Who can create each type: + - Personal clones are managed by the user who creates it, not visible for any other user. + - Team clones are managed by any ADMIN type team member (on the team member role attribute), + and all team admins will see it listed. + + - Events Module: + At enroll time, the clone is created and the authId is sent diff --git a/server/src/errors/index.js b/server/src/errors/index.js index 63b27a61a..74e7c36ff 100644 --- a/server/src/errors/index.js +++ b/server/src/errors/index.js @@ -53,6 +53,5 @@ export class KubernateError extends Error { message = 'OperationsModule Kubernate Error. ' + this.message constructor(message){ super(message) - logger.error(this.stack) } } diff --git a/server/src/graphQLCalls/authorizeClone.js b/server/src/graphQLCalls/authorizeClone.js new file mode 100644 index 000000000..da12c9879 --- /dev/null +++ b/server/src/graphQLCalls/authorizeClone.js @@ -0,0 +1,28 @@ +import logger from '../config/logger' +import axios from 'axios' + +const authorizeClon = (authorization, keyId, cloneId, releaseClon) => { + logger.debug('authorizeClon -> Entering function.') + + return axios({ + url: process.env.GATEWAY_ENDPOINT, + method: 'post', + data: { + query: ` + mutation keyVault_AuthorizeClone($keyId: String!, $cloneId: String!, $releaseClon: Boolean!){ + keyVault_AuthorizeClone(keyId: $keyId, cloneId: $cloneId, releaseClon:$releaseClon) + } + `, + variables: { + keyId: keyId, + cloneId: cloneId, + releaseClon: releaseClon + } + }, + headers: { + authorization: authorization + } + }) +} + +export default authorizeClon diff --git a/server/src/graphQLCalls/teamQuery.js b/server/src/graphQLCalls/teamQuery.js index c42c1722a..d5c3e32c4 100644 --- a/server/src/graphQLCalls/teamQuery.js +++ b/server/src/graphQLCalls/teamQuery.js @@ -1,8 +1,8 @@ import logger from '../config/logger' import axios from 'axios' -const teamQuery = async(authorization, teamId) => { - logger.debug('GraphQL retrieving team information. %s', teamId) +const teamQuery = (authorization, teamId) => { + logger.debug('teamQuery -> Entering function') return axios({ url: process.env.GATEWAY_ENDPOINT, diff --git a/server/src/kubernetes/createClone.js b/server/src/kubernetes/createClone.js index e4db79b8e..78e03c181 100644 --- a/server/src/kubernetes/createClone.js +++ b/server/src/kubernetes/createClone.js @@ -104,6 +104,11 @@ const createClone = async (clone) => { "name": "WAIT_TIME", "value": clone.waitTime.toString() }) + } else{ + env.push({ + "name": "KEY_ID", + "value": clone.keyId + }) } } else if(clone.botType === INDICATOR || clone.botType === EXTRACTION ){ if(clone.mode !== NO_TIME){ diff --git a/server/src/models/clone.js b/server/src/models/clone.js index e69df8262..d1071b4ec 100644 --- a/server/src/models/clone.js +++ b/server/src/models/clone.js @@ -23,6 +23,7 @@ const cloneSchema = new Schema({ type: String, required: true, }, + keyId: String, mode: { type: String, enum: CloneModeEnum, diff --git a/server/src/schema/cloneDetails.js b/server/src/schema/cloneDetails.js index 5ac7ba13b..22c10d69d 100644 --- a/server/src/schema/cloneDetails.js +++ b/server/src/schema/cloneDetails.js @@ -2,8 +2,8 @@ import logger from '../config/logger' import { isDefined } from '../config/utils' import { AutorizationError } from '../errors' -const cloneDetails = async(authId, team, clone) => { - logger.debug('Retrieving clone details.') +const cloneDetails = (authId, team, clone) => { + logger.debug('cloneDetails -> Entering function.') if(!(authId === (process.env.AUTH_CLIENT_ID + '@clients') || isAuthorized(team, authId))){ logger.debug('User %s is not authorized to manage team %s.', authId, team.slug ) @@ -22,12 +22,12 @@ const cloneDetails = async(authId, team, clone) => { clone.teamAvatar = team.profile.avatar clone.userLoggedIn = team.members[0].member.alias //TODO change by context info if possible - logger.debug('Retrieving clone details complete.') + logger.debug('cloneDetails -> Retrieving clone details complete.') return clone }; function getSelectedBot(team, selectedBotId){ - logger.debug('Getting selected bot.') + logger.debug('cloneDetails -> Getting selected bot.') if(isDefined(team)){ for (var i = 0; i < team.fb.length; i++) { if(team.fb[i].id === selectedBotId){ @@ -38,7 +38,7 @@ function getSelectedBot(team, selectedBotId){ } function isAuthorized(team, authId){ - logger.debug('Checking if the user is authorized by the teams module.') + logger.debug('cloneDetails -> Checking if the user is authorized by the teams module.') let authorized = false if(isDefined(team)){ for (var i = 0; i < team.members.length; i++) { diff --git a/server/src/schema/mutations/AddCloneMutation.js b/server/src/schema/mutations/AddCloneMutation.js index a0f9db02e..072c4f8df 100644 --- a/server/src/schema/mutations/AddCloneMutation.js +++ b/server/src/schema/mutations/AddCloneMutation.js @@ -1,23 +1,26 @@ -import { CloneInputType } from '../types/input'; -import { CloneType } from '../types'; -import { Clone } from '../../models'; -import { CloneModeEnum } from '../../enums/CloneMode'; -import { BotTypesEnum } from '../../enums/BotTypes'; +import { CloneInputType } from '../types/input' +import { CloneType } from '../types' +import { Clone } from '../../models' +import { CloneModeEnum } from '../../enums/CloneMode' +import { BotTypesEnum } from '../../enums/BotTypes' import teamQuery from '../../graphQLCalls/teamQuery' +import authorizeClone from '../../graphQLCalls/authorizeClone' import cloneDetails from '../cloneDetails' import { AuthentificationError, WrongArgumentsError, - OperationsError + OperationsError, + AutorizationError } from '../../errors' import logger from '../../config/logger' import createKubernetesClone from '../../kubernetes/createClone' import { isDefined } from '../../config/utils' +import { LIVE } from '../../enums/CloneMode' const args = { - clone: {type: CloneInputType } + clone: { type: CloneInputType } } const resolve = async(parent, { clone }, context) => { @@ -27,40 +30,44 @@ const resolve = async(parent, { clone }, context) => { throw new AuthentificationError() } - if(!Object.values(CloneModeEnum).includes(clone.mode)){ + if (!Object.values(CloneModeEnum).includes(clone.mode)) { throw new WrongArgumentsError('The mode selected is not valid.') } - if(!Object.values(BotTypesEnum).includes(clone.botType)){ + if (!Object.values(BotTypesEnum).includes(clone.botType)) { throw new WrongArgumentsError('The bot type selected is not valid.') } - try{ + if (clone.mode === LIVE && !isDefined(clone.keyId)) { + throw new WrongArgumentsError('The key was not provided to run the clone in Live mode.') + } + + try { let team = await teamQuery(context.authorization, clone.teamId) clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone) - clone.createDatetime = new Date().valueOf() / 1000|0 + clone.createDatetime = new Date().valueOf() / 1000 | 0 clone.active = true - logger.debug('addClone -> Creating a new clone.') let newClone = new Clone(clone) newClone.authId = context.userId clone.id = newClone._id - await createKubernetesClone(clone) - return new Promise((resolve, reject) => { - newClone.save((err) => { - if (err){ - logger.error('addClone -> Error: %s', err.stack) - reject('There has been an error storing the clone.') - } - else { - //TODO transaction on database - logger.debug('addClone -> Save clone sucessful.') - resolve(newClone) - } - }) - }) - } catch(err) { + logger.debug('addClone -> Creating the clone on the Database.') + let savedClone = await newClone.save() + + if (clone.mode === LIVE) { + logger.debug('addClone -> Authorizing clone to use the key.') + let response = await authorizeClone(context.authorization, clone.keyId, clone.id, false) + logger.debug('addClone -> Authorizing clone to use the key.') + if (response.data.data.keyVault_AuthorizeClone === clone.keyId) { + await createKubernetesClone(clone) + } else { + throw new AutorizationError() + } + } + logger.debug('addClone -> Clone created sucessfully.') + return savedClone + } catch (err) { logger.error('addClone -> Error: %s', err.stack) throw new OperationsError('There has been an error creating the clone.') } diff --git a/server/src/schema/mutations/RemoveCloneMutation.js b/server/src/schema/mutations/RemoveCloneMutation.js index 2efa137c0..32f33c3c3 100644 --- a/server/src/schema/mutations/RemoveCloneMutation.js +++ b/server/src/schema/mutations/RemoveCloneMutation.js @@ -40,6 +40,9 @@ const resolve = async (parent, { id }, context) => { let team = await teamQuery(context.authorization, clone.teamId) clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone) + logger.debug('removeClone -> Release the clone key.') + await authorizeClone(context.authorization, clone.keyId, clone.id, true) + logger.debug('removeClone -> Removing Clone from Kubernates.') await removeKuberneteClone(clone) diff --git a/server/src/schema/mutations/UpdateExecutionSummary.js b/server/src/schema/mutations/UpdateExecutionSummary.js index 1c889198d..4e46d5f91 100644 --- a/server/src/schema/mutations/UpdateExecutionSummary.js +++ b/server/src/schema/mutations/UpdateExecutionSummary.js @@ -13,6 +13,7 @@ import { import { Clone } from '../../models' import logger from '../../config/logger' import teamQuery from '../../graphQLCalls/teamQuery' +import authorizeClone from '../../graphQLCalls/authorizeClone' import cloneDetails from '../cloneDetails' import removeKuberneteClone from '../../kubernetes/removeClone' import { isDefined } from '../../config/utils' @@ -55,9 +56,6 @@ const resolve = async (parent, let team = await teamQuery(context.authorization, clone.teamId) clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone) - logger.debug('UpdateExecutionSummary -> Removing Clone from Kubernates.') - await removeKuberneteClone(clone) - const query = { _id: id } const options = { new: true } const update = { diff --git a/server/src/schema/queries/GetClones.js b/server/src/schema/queries/GetClones.js index 7125edc7b..020b02474 100644 --- a/server/src/schema/queries/GetClones.js +++ b/server/src/schema/queries/GetClones.js @@ -18,7 +18,7 @@ const args = { } const resolve = async(parent, { cloneIdList }, context) => { - logger.debug('Get Clones -> Entering Fuction. %j', cloneIdList) + logger.debug('Get Clones -> Entering Fuction.') try{ let clones = [] for (var i = 0; i < cloneIdList.length; i++) { @@ -44,7 +44,6 @@ const resolve = async(parent, { cloneIdList }, context) => { id: cloneIdList[i] }) } - } return clones diff --git a/server/src/schema/types/CloneType.js b/server/src/schema/types/CloneType.js index dfcd10a68..e06819625 100644 --- a/server/src/schema/types/CloneType.js +++ b/server/src/schema/types/CloneType.js @@ -37,7 +37,8 @@ const CloneType = new GraphQLObjectType({ teamName: {type: GraphQLString}, botName: {type: GraphQLString}, botAvatar: {type: GraphQLString}, - teamAvatar: {type: GraphQLString} + teamAvatar: {type: GraphQLString}, + keyId: {type: GraphQLString} }) }) export default CloneType diff --git a/server/src/schema/types/input/Clone.js b/server/src/schema/types/input/Clone.js index a4a6d95ec..8b40dd2ce 100644 --- a/server/src/schema/types/input/Clone.js +++ b/server/src/schema/types/input/Clone.js @@ -23,7 +23,8 @@ const Type = new GraphQLInputObjectType({ month: { type: GraphQLInt }, interval: { type: GraphQLInt }, processName: { type: new GraphQLNonNull(GraphQLString) }, - botType: { type: new GraphQLNonNull(GraphQLString) } + botType: { type: new GraphQLNonNull(GraphQLString) }, + keyId: { type: GraphQLString } }) });