diff --git a/api/.babelrc b/api/.babelrc index 1320b9a32..81bfbad3f 100644 --- a/api/.babelrc +++ b/api/.babelrc @@ -1,3 +1,7 @@ { - "presets": ["@babel/preset-env"] + "presets": ["@babel/preset-env"], + "plugins": [ + "@babel/plugin-proposal-throw-expressions", + ["@babel/plugin-proposal-class-properties", { "loose": true }] + ] } diff --git a/api/.env.sample b/api/.env.sample index 089a15249..6d0e4ce91 100644 --- a/api/.env.sample +++ b/api/.env.sample @@ -5,6 +5,8 @@ AUTH0_DOMAIN=auth0-domain AUTH0_AUDIENCE=auth0-audience AUTH0_ISSUER=auth0-issuer +PRESHARED_GATEWAY_KEY=SECRET_STUFF + NODE_ENV=development GENERATE_SOURCEMAP=false COMPOSE_HTTP_TIMEOUT=2000 diff --git a/api/package-lock.json b/api/package-lock.json index d5e74e85c..3c93e902b 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -575,6 +575,20 @@ "@babel/plugin-syntax-async-generators": "^7.0.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.1.0.tgz", + "integrity": "sha512-/PCJWN+CKt5v1xcGn4vnuu13QDoV+P7NcICP44BoonAJoPSGwVkgrXihFIQGiEjjPlUDBIw1cM7wYFLARS2/hw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0", + "@babel/plugin-syntax-class-properties": "^7.0.0" + } + }, "@babel/plugin-proposal-json-strings": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz", @@ -602,6 +616,16 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.0.0" } }, + "@babel/plugin-proposal-throw-expressions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.0.0.tgz", + "integrity": "sha512-CA2EUiwnbXrsdV4hy3jYghm91WaL7zL7xYfu628dyItRr6gylbRxshghGEK/Hhm//rR58N3PBmEeuYqSW57IUQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-throw-expressions": "^7.0.0" + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz", @@ -620,6 +644,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-class-properties": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.0.0.tgz", + "integrity": "sha512-cR12g0Qzn4sgkjrbrzWy2GE7m9vMl/sFkqZ3gIpAQdrvPDnLM8180i+ANDFIXfjHo9aqp0ccJlQ0QNZcFUbf9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz", @@ -644,6 +677,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-throw-expressions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.0.0.tgz", + "integrity": "sha512-/5uORdWlPta/ALhI5zKtm0Y9vAYOa7HJMML0OnCGk9XZA4hpGjb0Xjt/OVDCJVawC/4FrlAGCHOaj9BtWeVDvg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz", @@ -1606,6 +1648,15 @@ "protobufjs": "^6.8.6" } }, + "apollo-errors": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz", + "integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==", + "requires": { + "assert": "^1.4.1", + "extendable-error": "^0.1.5" + } + }, "apollo-link": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.2.tgz", @@ -1892,6 +1943,14 @@ "safer-buffer": "~2.1.0" } }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3781,6 +3840,11 @@ } } }, + "extendable-error": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz", + "integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20=" + }, "external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", @@ -10479,6 +10543,21 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/api/package.json b/api/package.json index 057991f3d..a4930bc5d 100644 --- a/api/package.json +++ b/api/package.json @@ -14,6 +14,7 @@ "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.1.0", "@babel/register": "^7.0.0", + "apollo-errors": "^1.9.0", "apollo-server-express": "^2.1.0", "axios": "^0.18.0", "cors": "^2.8.4", @@ -37,7 +38,10 @@ "winston": "^3.1.0" }, "devDependencies": { + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-proposal-throw-expressions": "^7.0.0", "graphql-cli": "^2.16.5", + "lodash.merge": "^4.6.1", "npm-run-all": "4.1.3", "prisma": "^1.18.1" } diff --git a/api/src/ApolloServer.js b/api/src/ApolloServer.js index 6130a0432..c71387463 100644 --- a/api/src/ApolloServer.js +++ b/api/src/ApolloServer.js @@ -1,6 +1,7 @@ import http from 'http' import { ApolloServer, makeExecutableSchema } from 'apollo-server-express' import { applyMiddleware as applyGraphQLMiddleware } from 'graphql-middleware' +import { moduleAuth } from './middleware' import { logger } from './logger' /** @@ -75,12 +76,12 @@ export default function createApolloServer( subscriptions: { path: subscriptionsEndpoint, onConnect: async (connection, websocket) => { - const { authorization } = connection + const { authorization, userid } = connection let contextData = {} try { // Simulate `req` object for auth - const req = { headers: { authorization } } + const req = { headers: { authorization, userid } } // Call all middlewares in order and modify `req` await new Promise((resolve, reject) => @@ -97,11 +98,7 @@ export default function createApolloServer( req, }) } catch (err) { - if (err.status !== 401) { - logger.error(err) - } - - throw err + logger.error(err) } return contextData @@ -114,6 +111,7 @@ export default function createApolloServer( // Express middleware server.applyMiddleware({ + moduleAuth, app, cors, path: graphqlEndpoint, diff --git a/api/src/auth/middleware/getMember.js b/api/src/auth/middleware/getMember.js deleted file mode 100644 index 7d19b2d6c..000000000 --- a/api/src/auth/middleware/getMember.js +++ /dev/null @@ -1,10 +0,0 @@ -const getMember = async (req, res, next, db) => { - if (!req.user) return next() - const member = await db.query.member({ - where: { authId: req.user.sub } - }) - req.user = { user: member } - next() -} - -module.exports = { getMember } diff --git a/api/src/auth/validateIdToken.js b/api/src/auth/validateIdToken.js deleted file mode 100644 index dd0b794b3..000000000 --- a/api/src/auth/validateIdToken.js +++ /dev/null @@ -1,28 +0,0 @@ -const jwksClient = require('jwks-rsa') -const jwt = require('jsonwebtoken') -const jwks = jwksClient({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 1, - jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json` -}) - -const validateIdToken = idToken => - new Promise((resolve, reject) => { - const { header, payload } = jwt.decode(idToken, { complete: true }) - if (!header || !header.kid || !payload) reject(new Error('Invalid Token')) - jwks.getSigningKey(header.kid, (err, key) => { - if (err) reject({ error: `Error getting signing key: ${err.message}` }) - jwt.verify( - idToken, - key.publicKey, - { algorithms: ['RS256'] }, - (err, decoded) => { - if (err) reject('jwt verify error: ' + err.message) - resolve(decoded) - } - ) - }) - }) - -module.exports = { validateIdToken } diff --git a/api/src/directives/index.js b/api/src/directives/index.js index 8e6b925ae..4d5da93a0 100644 --- a/api/src/directives/index.js +++ b/api/src/directives/index.js @@ -1,13 +1,12 @@ import { SchemaDirectiveVisitor } from 'apollo-server-express' import _get from 'lodash.get' -import { logger } from '../logger' - -import { AuthenticationError, ForbiddenError } from '../errors' -import { validateIdToken } from '../auth/validateIdToken' +import { logger, AuthenticationError, DatabaseError } from '../logger' const memberLocationOnContext = 'request.user' +const userIdOnContext = 'request.headers.userid' const bearerAccessToken = 'request.headers.authorization' +const ctxUserId = ctx => _get(ctx, userIdOnContext) const ctxMember = ctx => _get(ctx, memberLocationOnContext) const ctxToken = ctx => _get(ctx, bearerAccessToken) @@ -29,48 +28,21 @@ const createMember = async (ctx, idToken, info) => { return member } -const isLoggedIn = async ctx => { - logger.info(`isLoggedIn 0: ${JSON.stringify(ctx.request.headers)}`) - let member = ctxMember(ctx, memberLocationOnContext) - let token = ctxToken(ctx, bearerAccessToken) - let memberToken - logger.info('isLoggedIn: ', member, token) - if (!member && token) { - let scheme, credentials - const tokenParts = token.split(' ') - if (tokenParts.length === 2) { - scheme = tokenParts[0] - credentials = tokenParts[1] - } - - if (/^Bearer$/i.test(scheme)) { - token = credentials - //verify token - try { - memberToken = await validateIdToken(token) - const authId = memberToken.sub - exists = await ctx.db.query.member({ where: { authId: authId } }) - if (!member && exists === null) { - member = await createMember(ctx, memberToken).then(res => { - return res - }) - - return member - } - return member - } catch (err) { - memberToken = false - } - } - } - if (!member) { - throw new AuthenticationError('Access token is missing or expired') - } +const isUserId = async ctx => { + logger.info(`isUserId 0: ${ctx.request.headers.userid}`) + logger.info(`isUserId 1: ${ctx.request}`) + let member = await ctxUserId(ctx) + logger.info(member) + if (!member) { + throw new AuthenticationError() + return + } else { return member + } } -const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, typeId }) => - ctx.db.exists[type]({ slug: typeId, owner: memberId }) +const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, teamSlug }) => + ctx.db.exists[type]({ slug: teamSlug, owner: memberId }) const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ authId: memberId }) @@ -89,65 +61,68 @@ const assertAuth = ctx => { // Directive resolvers (apollo v1 syntax) export const directiveResolvers = { isAuthenticated: async (next, source, args, ctx) => { - let result = await isLoggedIn(ctx) - logger.info('directive isAuthenticated: ', result) + let result = await isUserId(ctx) + logger.info('directive isAuthenticated: ') + logger.info(await result) return next(result) }, hasRole: (next, source, { roles }, ctx) => { - const { role } = isLoggedIn(ctx) + const { role } = isUserId(ctx) logger.info('directive hasRole: ', role) if (roles.includes(role)) { return next() } - throw new ForbiddenError('Insufficient permissions') + throw new AuthenticationError('Insufficient permissions') }, isOwner: async (next, source, { type }, ctx) => { logger.info('directive isOwner: ', source, type, ctx) - const { slug: typeId } = - source && source.id - ? source - : ctx.request.body.variables ? ctx.request.body.variables : { id: null } - const user = await isLoggedIn(ctx) - const memberId = user.sub + logger.info(ctx.request.body.variables) + logger.info(type) + const { _v0_slug: typeId } = ctx.request.body.variables ? ctx.request.body.variables : { _v0_slug: null } + logger.info('typeId') + logger.info(typeId) + const userId = await isUserId(ctx) const isOwner = type === `Member` ? memberId === typeId - : await isRequestingMemberAlsoOwner({ ctx, memberId, type, typeId }) + : await isRequestingMemberAlsoOwner({ ctx, userId, type, typeId }) + logger.info('isOwner') + logger.info(isOwner) if (isOwner) { return next() } - throw new ForbiddenError('Insufficient permissions') + throw new AuthenticationError('Insufficient permissions') }, isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => { - const { authId: memberId } = await isLoggedIn(ctx) - logger.info('directive isOwnerOrHasRole 1: ', memberId, roles, type) - if(memberId === undefined && role=== undefined) throw new Error(`Not logged in`) + const userId = await isUserId(ctx) + logger.info('directive isOwnerOrHasRole 1: ', userId, roles, type) + if(userId === undefined && role=== undefined) throw new Error(`Not logged in`) if (roles.includes(role)) { return next() } - const { authId: typeId } = ctx.request.body.variables + const { teamSlug } = ctx.request.body.variables const isOwner = await isRequestingMemberAlsoOwner({ ctx, - memberId, + userId, type, - typeId + teamSlug }) - logger.info('directive isOwnerOrHasRole 2: ', typeId, memberId, role, isOwner) + logger.info('directive isOwnerOrHasRole 2: ', teamSlug, userId, role, isOwner) if (isOwner) { return next() } const hasRole = await isRequestingMember({ ctx, - memberId, + userId, type, - typeId + teamSlug }) if(hasRole){ return next() } - throw new ForbiddenError('Insufficient permissions') + throw new AuthenticationError('Insufficient permissions') } } diff --git a/api/src/errors/index.js b/api/src/errors/index.js index e55a27cc7..bb10ce39d 100644 --- a/api/src/errors/index.js +++ b/api/src/errors/index.js @@ -1,4 +1,5 @@ import { ApolloError } from 'apollo-server-express' +import { inspect } from 'util' import { logger } from '../logger' export { @@ -10,12 +11,32 @@ export { ValidationError, } from 'apollo-server-express' -export function formatError(error) { +export const formatError = (error) => { // This can be used to send errors to third // party services like Sentry or Stackdriver - logger.error(error) + logger.info('formatError') + logger.error(JSON.parse(JSON.stringify(inspect(error)))) + const originalError = searchOriginalError(error) + logger.error('originalError') + logger.error(originalError) + const { message, code } = originalError + return { message, code} +} - return error +const searchOriginalError = (error) => { + if (error.originalError) { + logger.error('Original Error') + logger.error(error.originalError) + return error.originalError + } + if (error.errors) { + logger.error('Errors') + logger.error(JSON.parse(JSON.stringify(inspect(error.errors)))) + return error.errors.map(searchOriginalError)[0] + } + logger.error('searchOriginalErrors') + logger.error(JSON.parse(JSON.stringify(inspect(error)))) + return error; } // Define custom Apollo errors here diff --git a/api/src/errors/notAllowed.json b/api/src/errors/notAllowed.json new file mode 100644 index 000000000..b76604aa2 --- /dev/null +++ b/api/src/errors/notAllowed.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "code": 401, + "message": "Wait, you're not supposed to be here, go through the gate as everyone else" + } + ] +} diff --git a/api/src/graphql/TeamFBFragment.js b/api/src/graphql/fragments/TeamFBFragment.js similarity index 90% rename from api/src/graphql/TeamFBFragment.js rename to api/src/graphql/fragments/TeamFBFragment.js index f7030921d..b0f99c664 100644 --- a/api/src/graphql/TeamFBFragment.js +++ b/api/src/graphql/fragments/TeamFBFragment.js @@ -1,4 +1,4 @@ -const gql = require('graphql-tag') +import gql from 'graphql-tag' const TEAM_FB_FRAGMENT = gql`{ id diff --git a/api/src/graphql/TeamsConnectionsFragment.js b/api/src/graphql/fragments/TeamsConnectionsFragment.js similarity index 95% rename from api/src/graphql/TeamsConnectionsFragment.js rename to api/src/graphql/fragments/TeamsConnectionsFragment.js index 5193475e0..f019dd02e 100644 --- a/api/src/graphql/TeamsConnectionsFragment.js +++ b/api/src/graphql/fragments/TeamsConnectionsFragment.js @@ -1,4 +1,4 @@ -const gql = require('graphql-tag') +import gql from 'graphql-tag' const TEAMS_CONNECTIONS_FRAGMENT = gql`{ edges { diff --git a/api/src/graphql/TeamsFragment.js b/api/src/graphql/fragments/TeamsFragment.js similarity index 93% rename from api/src/graphql/TeamsFragment.js rename to api/src/graphql/fragments/TeamsFragment.js index 442829e3c..1dd527af9 100644 --- a/api/src/graphql/TeamsFragment.js +++ b/api/src/graphql/fragments/TeamsFragment.js @@ -1,4 +1,4 @@ -const gql = require('graphql-tag') +import gql from 'graphql-tag' const TEAMS_FRAGMENT = gql`{ id diff --git a/api/src/graphql/resolvers/Mutation/Member.js b/api/src/graphql/resolvers/Mutation/Member.js new file mode 100644 index 000000000..653764d4c --- /dev/null +++ b/api/src/graphql/resolvers/Mutation/Member.js @@ -0,0 +1,17 @@ +export const createMember = async function (ctx, info, authId) { + logger.info('createMember', authId) + const member = await ctx.db.mutation.upsertMember({ + where: { + authId: authId, + }, + create: { + authId: authId, + alias: idToken.nickname + }, + update: { + authId: authId, + alias: idToken.nickname + } + }, info) + return member +} diff --git a/api/src/graphql/resolvers/Mutation/Team.js b/api/src/graphql/resolvers/Mutation/Team.js new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/graphql/resolvers/Mutation/TeamMember.js b/api/src/graphql/resolvers/Mutation/TeamMember.js new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/graphql/resolvers/Mutation/index.js b/api/src/graphql/resolvers/Mutation/index.js new file mode 100644 index 000000000..7afc8a29e --- /dev/null +++ b/api/src/graphql/resolvers/Mutation/index.js @@ -0,0 +1,188 @@ +import axios from 'axios' +import { ApolloError } from 'apollo-server-express' + +import { getUser } from '../../../middleware/getMember' + +import { sendTeamMemberInvite, sendTeamCreateConfirmation, verifyInviteToken } from '../../../email/sendgrid' + +import TEAMS_CONNECTIONS_FRAGMENT from '../../fragments/TeamsConnectionsFragment' +import TEAMS_FRAGMENT from '../../fragments/TeamsFragment' +import TEAM_FB_FRAGMENT from '../../fragments/TeamFBFragment' + +import { logger, AuthenticationError, ServiceUnavailableError, DatabaseError, ConflictError } from '../../../logger' + +export const resolvers = { + Mutation: { + async sendMemberInviteSG(parent, { email, teamId }, ctx, info) { + const team = await ctx.db.query.team({ where: { id: teamId } }, TEAMS_FRAGMENT) + const addMember = await ctx.db.mutation.upsertTeamMembers({ + where:{email: email}, + create:{role:'MEMBER', email:email, team: {connect: {id: teamId}}, status:{create:{status: 'INVITED', reason: `Invited by ${team.members[0].member.alias}`}}}, + update:{role: 'MEMBER', email: email, team: {connect: {id: teamId}}, status:{create:{status: 'INVITED', reason: `Invite resent by ${team.members[0].member.alias}`}}}}, + `{ id }`) + const sendInvite = await sendTeamMemberInvite(email, team) + return sendInvite + }, + async verifyTeamInvite(parent, { token }, ctx, info) { + let verifiedToken = null + let team = null + try { + verifiedToken = await verifyInviteToken(token) + team = { + email: verifiedToken.email, + team: { + slug: verifiedToken.team + } + } + logger.info('verifyTeamInvite.token: ', await verifiedToken) + } catch (err) { + logger.debug('verifyTeamInvite. err: ', err) + throw new DatabaseError(err.message) + } + return team + }, + async createTeam(parent, { name, slug, botName, botSlug }, ctx, info) { + logger.info('createTeam ctx.request.user:') + logger.info(ctx.request.headers.userid) + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + const userData = await getUser(authId) + logger.info('userData:') + logger.info(JSON.stringify(await userData)) + const alias = await userData.data.users_User.alias + const email = await userData.data.users_User.email + const avatar = 'https://aadevelop.blob.core.windows.net/module-teams/module-default/aa-avatar-default.png' + const banner = 'https://aadevelop.blob.core.windows.net/module-teams/module-default/aa-banner-default.png' + + const createTeamUrl = encodeURI(`${slug}/${name}/${alias}/${botSlug}/${botName}`) + logger.info('createTeamUrl:') + logger.info(JSON.stringify(await createTeamUrl)) + + const platformUrl = 'https://develop.advancedalgos.net/AABrowserAPI/newTeam/' + // const platformUrl = 'http://localhost:1337/AABrowserAPI/teamSetup/' + + logger.info(`${platformUrl}${createTeamUrl}/${authId}`) + const createPlatformTeam = await axios.get(`${platformUrl}${createTeamUrl}/${authId}`) + .then((result) => { + console.log('createPlatformTeam result:', result.data) + if(result.data.message === 'Team Name already taken'){ + return { error: 'Team already exist on AA Cloud.', code: 409 } + } + if(result.data.result === 'Fail'){ + return { error: `${result.data.message} - Creating team on AA Cloud has failed`, code: 404 } + } + return result + }) + + logger.info('createPlatformTeam') + logger.info(await createPlatformTeam) + logger.info('createPlatformTeam Error') + logger.error(await createPlatformTeam.error) + logger.info('createPlatformTeam Code') + logger.error(await createPlatformTeam.code) + + // if (await createPlatformTeam.error && await createPlatformTeam.code === 404) throw new ApolloError(await createPlatformTeam.error, 404) + // if (await createPlatformTeam.error && await createPlatformTeam.code === 409) throw new ApolloError(await createPlatformTeam.error, 409) + + const existingMember = await ctx.db.query.member({ where: { authId } }, `{id}`) + .catch((err) => { + logger.debug(err, 'existingMember error: ') + throw new DatabaseError(err) + }) + + logger.debug('existingMember:') + logger.debug(await existingMember) + + let createTeam + if(existingMember !== null && existingMember.id !== null){ + logger.info('createTeam with existingMember:') + createTeam = await ctx.db.mutation.createTeam({ data: {name: name, slug: slug, owner: authId, members: {create: {member: {connect: {authId: authId}}, role: 'OWNER', status: { create: { status: 'ACTIVE', reason: `Connected to Team ${name}`}}}}, profile: {create: {avatar: avatar, banner: banner}}, fb: {create: {name: botName, slug: botSlug, kind:'TRADER', avatar: avatar, status: {create: {status: 'ACTIVE', reason: "Cloned on team creation"}}}}, status: {create: {status: 'ACTIVE', reason:"Team created"}}} }, TEAMS_FRAGMENT) + .catch((err) => { + logger.debug(err, 'createTeamMutation error: ') + return new DatabaseError(err) + }) + } else { + logger.info('createTeam without existingMember:') + createTeam = await ctx.db.mutation.createTeam({ data: {name: name, slug: slug, owner: authId, members: {create: {member: {create: {authId: authId, alias: alias, visible:'true', status: { create: { status: 'ACTIVE', reason: `Created team ${name}`}}}}, role: 'OWNER', status: { create: { status: 'ACTIVE', reason: `Created with Team ${name}`}}}}, profile: {create: {avatar: avatar, banner: banner}}, fb: {create: {name: botName, slug: botSlug, kind:'TRADER', avatar: avatar, status: {create: {status: 'ACTIVE', reason: "Cloned on team creation"}}}}, status: {create: {status: 'ACTIVE', reason:"Team created"}}} }, TEAMS_FRAGMENT) + .catch((err) => { + logger.debug(err, 'createTeamMutation error: ') + return new DatabaseError(err) + }) + } + + sendTeamCreateConfirmation(email, name, botName) + + return createTeam + }, + async updateTeamProfile(parent, { slug, owner, description, motto, avatar, banner }, ctx, info) { + return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar, banner: banner}}}, where:{slug: slug}}, TEAMS_FRAGMENT) + .catch((err) => { + logger.debug('createTeam error: ', err) + throw new DatabaseError(`Team Profile Update Error: ${err}`) + }) + }, + async updateFB(parent, { fbId, avatar}, ctx, info) { + return ctx.db.mutation.updateFinancialBeings({data:{avatar:avatar}, where:{id:fbId}}, info) + .catch((err) => { + logger.debug('createTeam error: ', err) + throw new DatabaseError(`Update Financial Being Error: ${err}`) + }) + }, + async deleteTeam(parent, { slug, botSlug }, ctx, info) { + logger.info('deleteTeam ctx.request.user:') + logger.info(ctx.request.headers.userid) + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + logger.info(authId) + logger.info('getUser') + logger.info(getUser) + + return getUser(authId) + .then(async result => { + logger.info('deleteTeam axios:') + logger.info(await result) + + const alias = await result.data.users_User.alias + // const email = result.data.users_User.email + const deleteTeamUrl = encodeURI(`${slug}/${alias}/${botSlug}`) + logger.info('deleteTeamUrl:') + logger.info(JSON.stringify(await deleteTeamUrl)) + + const platformUrl = 'https://develop.advancedalgos.net/AABrowserAPI/deleteTeam/' + // const platformUrl = 'http://localhost:1337/AABrowserAPI/teamSetup/' + + logger.info(`${platformUrl}${deleteTeamUrl}/${authId}`) + const deletePlatformTeam = await axios.get(`${platformUrl}${deleteTeamUrl}/${authId}`) + .then((result) => { + console.log('deletePlatformTeam result:', result.data) + if(result.data.result === 'Fail'){ + throw new DatabaseError(result.data.message) + } + return result + }) + .catch(err =>{ + logger.debug('deletePlatformTeam err:') + logger.debug(err) + throw new DatabaseError(err) + }) + console.log('deletePlatformTeam returned: ', await deletePlatformTeam) + + return ctx.db.mutation.deleteTeam({ where: { slug } }, info) + .catch((res) => { + logger.info('deleteTeam error: ', res) + const errors = res.graphQLErrors.map((error) => { + throw new DatabaseError(error.message) + }) + }) + }) + .catch((err) => { + logger.debug('deleteTeam error: ') + throw new DatabaseError(err) + }) + } + } +} diff --git a/api/src/graphql/resolvers/Query/Member.js b/api/src/graphql/resolvers/Query/Member.js new file mode 100644 index 000000000..5f6e069b6 --- /dev/null +++ b/api/src/graphql/resolvers/Query/Member.js @@ -0,0 +1,31 @@ +import { logger, AuthenticationError, DatabaseError } from '../../../logger' + +export const member = async (parent, arg, ctx, info) => { + logger.info(' --> resolver.member') + logger.info(ctx.request.headers.userid) + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + return ctx.db.query.member({ where: { authId } }, info) + .catch(err => { + logger.debug('resolver.member error: ') + logger.debug(err) + throw new DatabaseError(`Member Data Error: ${err.message}`) + }) +} + +export const owner = async (parent, args, ctx, info) => { + logger.info('resolver.query.owner ctx: ') + logger.info(ctx.request.headers.userid) + const authId = cctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + return ctx.db.query.member({ where: { authId: authId } }, info) + .catch(err => { + logger.debug('resolver.owner error: ') + logger.debug(err) + throw new DatabaseError(`Member Data Error: ${err.message}`) + }) +} diff --git a/api/src/graphql/resolvers/Query/Team.js b/api/src/graphql/resolvers/Query/Team.js new file mode 100644 index 000000000..d70de3cd4 --- /dev/null +++ b/api/src/graphql/resolvers/Query/Team.js @@ -0,0 +1,53 @@ +import { logger, AuthenticationError, DatabaseError } from '../../../logger' + +import TEAMS_CONNECTIONS_FRAGMENT from '../../fragments/TeamsConnectionsFragment' +import TEAMS_FRAGMENT from '../../fragments/TeamsFragment' + +export const teams = async (parent, args, ctx, info) => { + logger.info('teams') + console.log('teams') + return ctx.db.query.teamsConnection({}, TEAMS_CONNECTIONS_FRAGMENT) +} + +export const teamById = async (parent, { teamId }, ctx, info) => { + logger.info('teamById') + logger.info(teamId) + return ctx.db.query.team({ where: { id: teamId } }, info) +} + +export const teamByName = async (parent, { name }, ctx, info) => { + return ctx.db.query.team({ where: { name: name } }, `{ name }`) +} + +export const teamWithRole = async (parent, { teamId, role }, ctx, info) => { + return ctx.db.query.teamsConnection({where: {AND: [{id: teamId},{members_some:{role: role}}]}, first:1}, info) +} + +export const teamsByOwner = async (parent, args, ctx, info) => { + logger.info('teamsByOwner') + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + return + } + return ctx.db.query.teams({where: { owner: authId }, orderBy:'updatedAt_DESC'}, TEAMS_FRAGMENT) +} + +export const teamsByRole = async (parent, args, ctx, info) => { + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + let teamAdmin + try { + teamAdmin= await ctx.db.query.teams({where: {members_some: {AND: [{member: {authId: authId}, role_not:'MEMBER'}]}}}, TEAMS_FRAGMENT) + } catch (err) { + logger.debug('teamsByRole error: ') + logger.debug(err) + const errors = err.graphQLErrors.map((error) => { + throw new DatabaseError(error.message) + }) + } + logger.info('teamsByRole teamAdmin: ', teamAdmin) + return teamAdmin +} diff --git a/api/src/graphql/resolvers/Query/TeamMember.js b/api/src/graphql/resolvers/Query/TeamMember.js new file mode 100644 index 000000000..1a8416c1f --- /dev/null +++ b/api/src/graphql/resolvers/Query/TeamMember.js @@ -0,0 +1,14 @@ +import { logger } from '../../../logger' + +import TEAM_FB_FRAGMENT from '../../fragments/TeamFBFragment' + +export const fbByTeamMember = async (parent, args, ctx, info) => { + logger.info('fbByTeamMember: ', args, ctx.request, ctx.request.res.req.user) + const authId = ctx.request.headers.userid + if (!authId) { + throw new AuthenticationError() + } + let teamMemberFB = await ctx.db.query.teams({where: {members_some: {AND: [{member: {authId: authId}}]}}, orderBy: 'updatedAt_DESC'}, TEAM_FB_FRAGMENT) + logger.info('fbByTeamMember response: ', await teamMemberFB[0]) + return teamMemberFB[0] +} diff --git a/api/src/graphql/resolvers/Query/index.js b/api/src/graphql/resolvers/Query/index.js new file mode 100644 index 000000000..5616f8cbb --- /dev/null +++ b/api/src/graphql/resolvers/Query/index.js @@ -0,0 +1,19 @@ +import { logger } from '../../../logger' + +import { member, owner } from './Member' +import { teams, teamById, teamByName, teamWithRole, teamsByOwner, teamsByRole } from './Team' +import{ fbByTeamMember } from './TeamMember' + +export const resolvers = { + Query: { + member: (parent, arg, ctx, info) => member(parent, arg, ctx, info), + teams: (parent, arg, ctx, info) => teams(parent, arg, ctx, info), + teamById: (parent, arg, ctx, info) => teamById(parent, arg, ctx, info), + teamByName: (parent, arg, ctx, info) => teamByName(parent, arg, ctx, info), + teamWithRole: (parent, arg, ctx, info) => teamWithRole(parent, arg, ctx, info), + teamsByOwner: (parent, arg, ctx, info) => teamsByOwner(parent, arg, ctx, info), + teamsByRole: (parent, arg, ctx, info) => teamsByRole(parent, arg, ctx, info), + owner: (parent, arg, ctx, info) => owner(parent, arg, ctx, info), + fbByTeamMember: (parent, arg, ctx, info) => fbByTeamMember(parent, arg, ctx, info) + } +} diff --git a/api/src/graphql/resolvers/index.js b/api/src/graphql/resolvers/index.js new file mode 100644 index 000000000..543ab4e32 --- /dev/null +++ b/api/src/graphql/resolvers/index.js @@ -0,0 +1,6 @@ +import merge from 'lodash.merge' + +import { resolvers as queryResolvers } from './Query' +import { resolvers as mutationResolvers } from './Mutation' + +export const resolvers = merge(queryResolvers, mutationResolvers) diff --git a/api/src/schema.graphql b/api/src/graphql/schema.graphql similarity index 72% rename from api/src/schema.graphql rename to api/src/graphql/schema.graphql index 2be3053e3..2095882bb 100644 --- a/api/src/schema.graphql +++ b/api/src/graphql/schema.graphql @@ -1,4 +1,4 @@ -# import Query.TeamConnection, Team from "./generated/prisma.graphql" +# import Query.TeamConnection, Team from "../generated/prisma.graphql" directive @isAuthenticated on QUERY | FIELD | MUTATION directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION @@ -11,12 +11,11 @@ type Query { teamByName(name: String!): Team teamById(teamId: String!): Team teamWithRole(teamId: String!, role: String!): Team - teamsByOwner(ownerId: String!): [Team!]! @isAuthenticated + teamsByOwner: [Team!]! @isAuthenticated teamsByRole: [Team!]! @isAuthenticated fbByTeamMember: Team @isAuthenticated owner: Member @isAuthenticated - currentMember: Member @isAuthenticated - member(authId: String!): Member @isAuthenticated + member: Member @isAuthenticated } type Mutation { @@ -25,7 +24,7 @@ type Mutation { createTeam(name: String!, slug: String!, botName: String!, botSlug: String!): Team @isAuthenticated updateTeamProfile(slug: String!, description: String, motto: String, avatar: String, banner: String): Team @isAuthenticated updateFB(fbId: String!, avatar: String): FinancialBeings @isAuthenticated - deleteTeam(slug: String!): Team @isAuthenticated @isOwner(type: "Team") + deleteTeam(slug: String!, botSlug: String!): Team @isAuthenticated @isOwner(type: "Team") sendMemberInviteSG(email: String!, teamId: String!): String confirmMember(token: String!): String removeTeamMember(teamId: String!, memberId: String!): String @@ -35,18 +34,14 @@ fragment MemberInfo on Member { id authId alias - isActive - profile { - ...MemberProfileInfo + visible + status { + ...MemberStatusInfo } } -fragment MemberProfileInfo on MemberProfile { - firstName - lastName - email - avatar - role { - name - } +fragment MemberStatusInfo on MemberStatus { + status + reason + createdAt } diff --git a/api/src/index.js b/api/src/index.js index 43efb44cb..bdac90407 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -13,265 +13,20 @@ import { importSchema } from 'graphql-import' import parser from 'fast-xml-parser' import axios from 'axios' -import checkJwt from './auth/middleware/jwt' -import { getMember } from './auth/middleware/getMember' -import { validateIdToken } from './auth/validateIdToken' +import { resolvers } from './graphql/resolvers' + import { schemaDirectives } from './directives' -import { sendTeamMemberInvite, sendTeamCreateConfirmation, verifyInviteToken } from './email/sendgrid' - -import TEAMS_FRAGMENT from './graphql/TeamsFragment' -import TEAMS_CONNECTIONS_FRAGMENT from './graphql/TeamsConnectionsFragment' -import TEAM_FB_FRAGMENT from './graphql/TeamFBFragment' - import pubsub from './pubsub' import { logger } from './logger' +import wrongPreshared from './errors/notAllowed.json' + const GRAPHQL_ENDPOINT = '/graphql' const GRAPHQL_SUBSCRIPTIONS = '/graphql' const PORT = 4001 const NODE_ENV = 'development' -const createMember = async function (ctx, info, idToken) { - logger.info('createMember', idToken) - const member = await ctx.db.mutation.upsertMember({ - where: { - authId: idToken.sub, - }, - create: { - authId: idToken.sub, - alias: idToken.nickname - }, - update: { - authId: idToken.sub, - alias: idToken.nickname - } - }, info) - return member -} - -const ctxMember = ctx => ctx.request.user - -const resolvers = { - Query: { - async member(parent, arg, ctx, info) { - logger.info('member: ', parent, arg) - return ctx.db.query.member({ where: { authId: arg.authId } }, info) - }, - async currentMember(parent, args, ctx, info) { - logger.info('currentMember: ', ctx.request.res.req.user, ctx.token) - const authId = ctx.request.res.req.user.sub - - return ctx.db.query.member({ where: { authId } }, info) - }, - teams(parent, args, ctx, info) { - logger.info('teams: ', ctx.request.res) - return ctx.db.query.teamsConnection({}, TEAMS_CONNECTIONS_FRAGMENT) - }, - async teamById(parent, { id }, ctx, info) { - return ctx.db.query.team({ where: { id } }, info) - }, - async teamByName(parent, { name }, ctx, info) { - return ctx.db.query.team({ where: { name: name } }, `{ name }`) - }, - async teamWithRole(parent, { teamId, role }, ctx, info) { - return ctx.db.query.teamsConnection({where: {AND: [{id: teamId},{members_some:{role: role}}]}, first:1}, info) - }, - async teamsByOwner(parent, { ownerId }, ctx, info) { - return ctx.db.query.teams({where: { owner: ownerId }, orderBy:'updatedAt_DESC'}, TEAMS_FRAGMENT) - }, - async teamsByRole(parent, args, ctx, info) { - const authId = ctx.request.user.sub - let teamAdmin = await ctx.db.query.teams({where: {members_some: {OR: [{role: 'ADMIN'}, {role: 'OWNER'}], AND: [{member: {authId: authId}}]}}, orderBy: 'updatedAt_DESC'}, TEAMS_FRAGMENT) - logger.info('teamsByRole teamAdmin: ', teamAdmin) - return teamAdmin - }, - async fbByTeamMember(parent, args, ctx, info) { - logger.info('fbByTeamMember: ', args, ctx.request, ctx.request.res.req.user) - const authId = ctx.request.user.sub - let teamMemberFB = await ctx.db.query.teams({where: {members_some: {AND: [{member: {authId: authId}}]}}, orderBy: 'updatedAt_DESC'}, TEAM_FB_FRAGMENT) - logger.info('fbByTeamMember response: ', await teamMemberFB[0]) - return teamMemberFB[0] - }, - async owner(parent, args, ctx, info) { - logger.info('resolver.query.owner ctx: ', ctxMember(ctx)) - return ctx.db.query.member({ where: { id: ctxMember(ctx).authId } }, info) - .catch(err => { - logger.debug('createTeam error: ', err) - const errors = res.graphQLErrors.map((error) => { - return error.message - }) - }) - } - }, - Mutation: { - async authenticate(parent, { idToken }, ctx, info) { - let memberToken = null - try { - memberToken = await validateIdToken(idToken) - logger.info('authenticate.memberToken: ', await memberToken) - } catch (err) { - logger.debug('authenticat.validateIdToken err: ', err) - throw new Error(err.message) - } - const authId = memberToken.sub - let member = await ctx.db.query.member({ where: { authId } }, info) - - if (!member) { - try { - return createMember(ctx, info, memberToken) - } catch (error) { - throw new Error(error.message) - } - } - return member - }, - async sendMemberInviteSG(parent, { email, teamId }, ctx, info) { - const team = await ctx.db.query.team({ where: { id: teamId } }, TEAMS_FRAGMENT) - const addMember = await ctx.db.mutation.upsertTeamMembers({ - where:{email: email}, - create:{role:'MEMBER', email:email, team: {connect: {id: teamId}}, status:{create:{status: 'INVITED', reason: `Invited by ${team.members[0].member.alias}`}}}, - update:{role: 'MEMBER', email: email, team: {connect: {id: teamId}}, status:{create:{status: 'INVITED', reason: `Invite resent by ${team.members[0].member.alias}`}}}}, - `{ id }`) - const sendInvite = await sendTeamMemberInvite(email, team) - return sendInvite - }, - async verifyTeamInvite(parent, { token }, ctx, info) { - let verifiedToken = null - let team = null - try { - verifiedToken = await verifyInviteToken(token) - team = { - email: verifiedToken.email, - team: { - slug: verifiedToken.team - } - } - logger.info('verifyTeamInvite.token: ', await verifiedToken) - } catch (err) { - logger.debug('verifyTeamInvite. err: ', err) - throw new Error(err.message) - } - return team - }, - async createTeam(parent, { name, slug, botName, botSlug }, ctx, info) { - logger.info(ctx.request.user, 'createTeam ctx.request.user:') - const authId = ctx.request.user.sub - - const encodedAuthId = encodeURI(authId) - const encodedURL = `https://users-api.advancedalgos.net/graphql?query=%7B%0A%20%20userByAuthId(authId:%20%22${authId}%22)%20%7B%0A%20%20%20%20id%0A%20%20%20%20email%0A%20%20%20%20alias%0A%20%20%7D%0A%7D%0A` - const decodedURL = decodeURI(encodedURL) - return getUser(authId) - .then(async result => { - logger.info('axios:') - logger.info(result) - - const alias = result.data.users_UserByAuthId.alias - const email = result.data.users_UserByAuthId.email - const avatar = 'https://aadevelop.blob.core.windows.net/module-teams/module-default/aa-avatar-default.png' - const banner = 'https://aadevelop.blob.core.windows.net/module-teams/module-default/aa-banner-default.png' - - const createTeamUrl = encodeURI(`${slug}/${name}/${alias}/${botSlug}/${authId}`) - logger.info('createTeamUrl:') - logger.info(JSON.stringify(await createTeamUrl)) - - const platformUrl = 'https://develop.advancedalgos.net/AABrowserAPI/teamSetup/' - // const platformUrl = 'http://localhost:3100/AABrowserAPI/teamSetup/' - const createPlatformTeam = await axios.get(`${platformUrl}${createTeamUrl}`) - .then((result) => { - logger.info('createPlatformTeam:', result) - if(result.data.message === 'Team Name already taken'){ - throw new Error(result.data.message) - } - return result - }) - .catch(err =>{ - logger.errror('createPlatformTeam err:', err) - throw new Error(err) - }) - logger.info('createPlatformTeam: ', await createPlatformTeam) - if(createPlatformTeam === 'Error: Team Name already taken'){ - return createPlatformTeam - } - const createTeam = await ctx.db.mutation.createTeam({ data: {name: name, slug: slug, owner: authId, members: {create: {member: {create: {authId: authId, alias: alias, visible:'true', status: { create: { status: 'ACTIVE', reason: `Created team ${name}`}}}}, role: 'OWNER'}}, profile: {create: {avatar: avatar, banner: banner}}, fb: {create: {name: botName, slug: botSlug, kind:'TRADER', avatar: avatar, status: {create: {status: 'ACTIVE', reason: "Cloned on team creation"}}}}, status: {create: {status: 'ACTIVE', reason:"Team created"}}} }, TEAMS_FRAGMENT) - .catch((err) => { - logger.debug(err, 'createTeam error: ') - return err - }) - - sendTeamCreateConfirmation(email, name, botName) - return createTeam - }) - .catch((err) => { - logger.debug('createTeam error: ') - return err - }) - }, - async updateTeamProfile(parent, { slug, owner, description, motto, avatar, banner }, ctx, info) { - return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar, banner: banner}}}, where:{slug: slug}}, TEAMS_FRAGMENT) - .catch((err) => { - logger.debug('createTeam error: ', err) - return err - }) - }, - async updateFB(parent, { fbId, avatar}, ctx, info) { - return ctx.db.mutation.updateFinancialBeings({data:{avatar:avatar}, where:{id:fbId}}, info) - .catch((err) => { - logger.debug('createTeam error: ', err) - return err - }) - }, - async deleteTeam(parent, { slug }, ctx, info) { - return ctx.db.mutation.deleteTeam({ where: { slug } }, info) - .catch((res) => { - logger.info('deleteTeam error: ', res) - const errors = res.graphQLErrors.map((error) => { - return error.message - }) - }) - } - } -} - -const getUser = authId => { - logger.info('getUser') - logger.info(authId) - const API_URL = 'https://app-api.advancedalgos.net/graphql' - return new Promise(resolve => { - try { - const result = axios.post(API_URL, { - query: `query users_UserByAuthId($authId: String!) { - users_UserByAuthId(authId: $authId){ - id - alias - email - } - }`, - variables: { - authId: authId - } - }, { - headers: { - 'Content-Type': 'application/json' - } - }) - .then(result => { - logger.info('getUser result') - logger.info(result.data) - resolve(result.data) - }) - .catch(err => { - logger.info('getUser err') - logger.info(err) - throw new Error(err) - }) - - } catch (error) { - resolve(error) - } - }) -} - const db = new Prisma({ fragmentReplacements: extractFragmentReplacements(resolvers), typeDefs: 'src/generated/prisma.graphql', @@ -282,20 +37,15 @@ const db = new Prisma({ const app = express() -const connectionMiddlewares = [ - checkJwt, - function (err, req, res, next) { - logger.info('checkJwt: ', err, req.headers, req.user) - if (err) { - return res.status(201).send(err.message) - } else { - next() - } - }, - // (req, res, done) => getMember(req, res, done, db) -] +app.post(GRAPHQL_ENDPOINT, (req, res, next) => { + if (req.headers.preshared === process.env.PRESHARED_GATEWAY_KEY) { + next(); + } else { + res.send(wrongPreshared); + } +}); -app.post(GRAPHQL_ENDPOINT, ...connectionMiddlewares) +app.post(GRAPHQL_ENDPOINT) const corsOptions = { "origin": "*", @@ -315,16 +65,16 @@ const options = { const server = createApolloServer(app, { graphqlEndpoint: GRAPHQL_ENDPOINT, subscriptionsEndpoint: GRAPHQL_SUBSCRIPTIONS, - wsMiddlewares: connectionMiddlewares, apolloServerOptions: { formatError }, - typeDefs: importSchema('src/schema.graphql'), + typeDefs: importSchema('src/graphql/schema.graphql'), resolvers, resolverValidationOptions: { requireResolversForResolveType: false }, schemaDirectives, context: req => ({ ...req , - token: req.headers, - user: req.user, + token: req.headers ? req.headers : undefined, + user: req.user ? req.user : undefined, + userId: (req.headers && req.headers.userid) ? req.headers.userid : undefined, db, pubsub }) @@ -335,5 +85,3 @@ server.listen({ port: PORT }, () => { `🚀 GraphQL Server is running on http://localhost:${PORT}${GRAPHQL_ENDPOINT} in "${NODE_ENV}" mode\n` ) }) - -module.exports= { createMember } diff --git a/api/src/logger/errors.js b/api/src/logger/errors.js new file mode 100644 index 000000000..14615ad7d --- /dev/null +++ b/api/src/logger/errors.js @@ -0,0 +1,24 @@ +export class AuthenticationError extends Error { + code = 401 + message = 'Not Authenticated - you have to be authenticated to perform this action' +} + +export class DatabaseError extends Error { + code = 404 + message = `Resource Error: ${this.message}` +} + +export class ApolloError extends Error { + code = 409 + message = `Resource already exists: ${this.message}` +} + +export class WrongArgumentsError extends Error { + code = 400 + message = 'Wrong arguments : ' + this.message +} + +export class ServiceUnavailableError extends Error { + code = 503 + message = 'At least one service is unresponding ' + this.message +} diff --git a/api/src/logger/index.js b/api/src/logger/index.js index 7d55674e9..e2b2cba06 100644 --- a/api/src/logger/index.js +++ b/api/src/logger/index.js @@ -1,4 +1,7 @@ import { createLogger, format, transports } from 'winston' +import { inspect } from 'util' + +import { AuthenticationError, ApolloError, DatabaseError, WrongArgumentsError, ServiceUnavailableError, ConflictError } from './errors' class LoggerService { constructor() { @@ -8,7 +11,7 @@ class LoggerService { format.colorize(), format.timestamp({ format: 'YY/MM/DD HH:mm' }), format.printf( - info => `${info.timestamp} - ${info.level}: ${JSON.stringify(info.message)}` + info => `${info.timestamp} - ${info.level}: ${JSON.parse(JSON.stringify(inspect(info.message)))}` ) ), }), @@ -27,4 +30,4 @@ class LoggerService { } export const logger = new LoggerService() // Default logger -export { LoggerService } +export { LoggerService, AuthenticationError, ApolloError, ConflictError, DatabaseError, WrongArgumentsError, ServiceUnavailableError } diff --git a/api/src/middleware/getMember.js b/api/src/middleware/getMember.js new file mode 100644 index 000000000..d88eaba8f --- /dev/null +++ b/api/src/middleware/getMember.js @@ -0,0 +1,57 @@ +import axios from 'axios' + +import { logger, DatabaseError } from '../logger' + +export const getMember = async (req, res, next, db) => { + if (!req.headers.userId) return next() + logger.info('getMember ') + logger.info(req.headers.userId) + try { + const member = await getUser(req.headers.userId) + req.user = await member + next() + } catch (err) { + next() + } + +} + +export const getUser = authId => { + logger.info('getUser MiddleWare') + logger.info(authId) + const API_URL = 'https://app-api.advancedalgos.net/graphql' + return new Promise((resolve, reject) => { + try { + return axios.post(API_URL, { + query: `query users_User($userId: ID!) { + users_User(id: $userId){ + id + alias + email + } + }`, + variables: { + userId: authId + } + }, { + headers: { + 'Content-Type': 'application/json' + } + }) + .then(res => { + logger.info('getUser result') + logger.info(res.data) + resolve(res.data) + }) + .catch(err => { + logger.info('getUser err') + logger.info(err) + reject(err) + }) + } catch (error) { + reject(error) + } + }) +} + +export default getUser diff --git a/api/src/middleware/index.js b/api/src/middleware/index.js new file mode 100644 index 000000000..c23d24ee0 --- /dev/null +++ b/api/src/middleware/index.js @@ -0,0 +1,3 @@ +import moduleAuth from './moduleAuth' + +export { moduleAuth } diff --git a/api/src/auth/middleware/jwt.js b/api/src/middleware/jwt.js similarity index 100% rename from api/src/auth/middleware/jwt.js rename to api/src/middleware/jwt.js diff --git a/api/src/middleware/moduleAuth.js b/api/src/middleware/moduleAuth.js new file mode 100644 index 000000000..6f1bb1509 --- /dev/null +++ b/api/src/middleware/moduleAuth.js @@ -0,0 +1,13 @@ +import { logger } from '../logger' + +export const moduleAuth = async (req, res, next, db) => { + if (req.headers.preshared === process.env.PRESHARED_GATEWAY_KEY) { + logger.info('Module authenticated') + next(); + } else { + logger.error('Module authentication error') + res.send(wrongPreshared); + } +} + +export default moduleAuth diff --git a/client/package.json b/client/package.json index 53982f016..258ac437d 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@advancedalgos/teams-client", - "version": "0.0.22", + "version": "0.0.24", "description": "Advanced Algos Team client-side web app", "main": "src/index.js", "scripts": { diff --git a/client/src/graphql/teams/DeleteTeamMutation.js b/client/src/graphql/teams/DeleteTeamMutation.js index 0db2a7ad4..ce02bc213 100644 --- a/client/src/graphql/teams/DeleteTeamMutation.js +++ b/client/src/graphql/teams/DeleteTeamMutation.js @@ -2,8 +2,8 @@ import gql from 'graphql-tag' export const DELETE_TEAM = gql` - mutation deleteTeam($slug: String!) { - teams_DeleteTeam(slug: $slug) { + mutation deleteTeam($slug: String!, $botSlug: String!) { + teams_DeleteTeam(slug: $slug, botSlug: $botSlug) { id } } diff --git a/client/src/graphql/teams/GetTeamsByOwnerQuery.js b/client/src/graphql/teams/GetTeamsByOwnerQuery.js index adbb28c4a..730769d8b 100644 --- a/client/src/graphql/teams/GetTeamsByOwnerQuery.js +++ b/client/src/graphql/teams/GetTeamsByOwnerQuery.js @@ -2,8 +2,8 @@ import gql from 'graphql-tag' export const GET_TEAMS_BY_OWNER = gql` - query teamsByOwnerQuery($authId: String!) { - teams_TeamsByOwner(ownerId: $authId) { + query teamsByOwnerQuery { + teams_TeamsByOwner { id name slug diff --git a/client/src/utils/graphql-errors.js b/client/src/utils/graphql-errors.js index 26b9c18ce..8ada52b66 100644 --- a/client/src/utils/graphql-errors.js +++ b/client/src/utils/graphql-errors.js @@ -1,12 +1,14 @@ +import log from './log' + const ErrorResponse = { 'unique constraint': 'Sorry, already taken! Please create a unique ' } export const checkGraphQLError = error => { - console.log('checkGraphQLError ', ErrorResponse) + log.debug('checkGraphQLError ', ErrorResponse) if (error.search(/unique constraint/)) { const field = error.substring(error.indexOf('=') + 1) - console.log('unique constraint', field.replace(/ +/g, '')) + log.debug('unique constraint', field.replace(/ +/g, '')) } } diff --git a/client/src/utils/log.js b/client/src/utils/log.js new file mode 100644 index 000000000..47e819934 --- /dev/null +++ b/client/src/utils/log.js @@ -0,0 +1,7 @@ +import minilog from 'minilog' + +if (process.env.NODE_ENV === 'development') minilog.enable() + +const log = minilog('frontend') + +export default log diff --git a/client/src/views/common/components/TeamBar.js b/client/src/views/common/components/TeamBar.js index b51f22e13..992f188fc 100644 --- a/client/src/views/common/components/TeamBar.js +++ b/client/src/views/common/components/TeamBar.js @@ -12,16 +12,32 @@ import GroupIcon from '@material-ui/icons/Group' import UsersIcon from '@material-ui/icons/People' import SearchIcon from '@material-ui/icons/Search' import AdbIcon from '@material-ui/icons/Adb' -import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications' +// import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications' +import BugReportIcon from '@material-ui/icons/BugReport' import { Link } from 'react-router-dom' -const SettingsLink = props => +import log from '../../../utils/log' + +// const SettingsLink = props => const FBLink = props => const MembersLink = props => const ManageLink = props => const ViewLink = props => +/* + +*/ + const styles = theme => ({ root: { flexGrow: 1 @@ -61,13 +77,13 @@ const styles = theme => ({ class TeamBar extends Component { render () { const { classes, user, match } = this.props - console.log('TeamBar: ', user, match) + log.debug('TeamBar: ', user, match) return (