TeamsModule moved to old repos

This commit is contained in:
Luis Fernando Molina committed 2020-11-20 19:41:19 +01:00
1 parent 2814d4307b
commit 8bf22b6bde
210 files changed

No files matched your search

@@ -0,0 +1,124 @@
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'
export default function createApolloServer(
app,
{
// Main options
graphqlEndpoint = '',
typeDefs = {},
resolvers = {},
schemaDirectives = {},
directiveResolvers = {},
graphqlMiddlewares = [],
dataSources = () => ({}),
context = () => ({}),
// Subscriptions
subscriptionsEndpoint = '',
wsMiddlewares = [],
// Mocks
mocks,
// Apollo Engine
engineKey = '',
// HTTP options
cors = true,
timeout = 120000,
// Extra options for Apollo Server
apolloServerOptions = {},
}
) {
const schema = makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives,
directiveResolvers,
resolverValidationOptions: {
requireResolversForResolveType: false,
},
})
// Apollo server options
const options = {
...apolloServerOptions,
schema: applyGraphQLMiddleware(schema, ...graphqlMiddlewares),
tracing: true,
cacheControl: true,
introspection: true,
playground: true,
engine: engineKey ? { apiKey: engineKey } : false,
dataSources,
// Resolvers context in POST requests
context: async ({ req, connection }) => {
let contextData
try {
if (connection) {
contextData = { ...connection.context }
} else {
contextData = await context({ req, request: req })
}
} catch (err) {
logger.error(err)
throw err
}
return contextData
},
// Resolvers context in WebSocket requests
subscriptions: {
path: subscriptionsEndpoint,
onConnect: async (connection, websocket) => {
const { authorization, userid } = connection
let contextData = {}
try {
// Simulate `req` object for auth
const req = { headers: { authorization, userid } }
// Call all middlewares in order and modify `req`
await new Promise((resolve, reject) =>
wsMiddlewares.reduceRight(
(acc, m) => err => (err ? reject(err) : m(req, null, acc)),
err => (err ? reject(err) : resolve())
)()
)
contextData = await context({
connection,
websocket,
request: req,
req,
})
} catch (err) {
logger.error(err)
}
return contextData
},
},
}
// Apollo Server
const server = new ApolloServer(options)
// Express middleware
server.applyMiddleware({
moduleAuth,
app,
cors,
path: graphqlEndpoint,
// gui: {
// endpoint: graphqlEndpoint,
// subscriptionEndpoint: graphqlSubscriptionsPath,
// },
})
// Create HTTP server and add subscriptions
const httpServer = http.createServer(app)
httpServer.setTimeout(timeout)
server.installSubscriptionHandlers(httpServer)
return httpServer
}
@@ -0,0 +1,163 @@
import { SchemaDirectiveVisitor } from 'apollo-server-express'
import _get from 'lodash/get'
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)
const createMember = async (ctx, idToken, info) => {
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 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, teamSlug }) =>
ctx.db.exists[type]({ slug: teamSlug, owner: memberId })
const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ authId: memberId })
const getUser = ({ req = {}, request = {} }) => req.user || request.user || null
const hasRole = (roles, ctx) => {
const { role = '' } = getUser(ctx) || {}
return role && roles.includes(role)
}
const assertAuth = ctx => {
if (!getUser(ctx)) {
throw new AuthenticationError('Access token is missing or expired')
}
}
// Directive resolvers (apollo v1 syntax)
export const directiveResolvers = {
isAuthenticated: async (next, source, args, ctx) => {
let result = await isUserId(ctx)
logger.info('directive isAuthenticated: ')
logger.info(await result)
return next(result)
},
hasRole: (next, source, { roles }, ctx) => {
const { role } = isUserId(ctx)
logger.info('directive hasRole: ', role)
if (roles.includes(role)) {
return next()
}
throw new AuthenticationError('Insufficient permissions')
},
isOwner: async (next, source, { type }, ctx) => {
logger.info('directive isOwner: ', source, type, ctx)
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, userId, type, typeId })
logger.info('isOwner')
logger.info(isOwner)
if (isOwner) {
return next()
}
throw new AuthenticationError('Insufficient permissions')
},
isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => {
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 { teamSlug } = ctx.request.body.variables
const isOwner = await isRequestingMemberAlsoOwner({
ctx,
userId,
type,
teamSlug
})
logger.info('directive isOwnerOrHasRole 2: ', teamSlug, userId, role, isOwner)
if (isOwner) {
return next()
}
const hasRole = await isRequestingMember({
ctx,
userId,
type,
teamSlug
})
if(hasRole){
return next()
}
throw new AuthenticationError('Insufficient permissions')
}
}
export const defaultFieldResolver = (source, args, contextValue, info) => {
// ensure source is a value for which property access is acceptable.
if (typeof source === 'object' || typeof source === 'function') {
const property = source[info.fieldName]
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info)
}
return property
}
}
// Schema directives (apollo v2 syntax)
export const schemaDirectives = Object.keys(directiveResolvers).reduce(
(acc, directiveName) => ({
...acc,
[directiveName]: class extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const resolver = directiveResolvers[directiveName]
const originalResolver = field.resolve || defaultFieldResolver
const directiveArgs = this.args
field.resolve = (...args) => {
const [source, , context, info] = args
return resolver(
async () => originalResolver.apply(field, args),
source,
directiveArgs,
context,
info
)
}
}
},
}),
{}
)
@@ -0,0 +1,49 @@
import { ApolloError } from 'apollo-server-express'
import { inspect } from 'util'
import { logger } from '../logger'
export {
ApolloError,
AuthenticationError,
ForbiddenError,
SyntaxError,
UserInputError,
ValidationError,
} from 'apollo-server-express'
export const formatError = (error) => {
// This can be used to send errors to third
// party services like Sentry or Stackdriver
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}
}
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
export class ExternalServiceError extends ApolloError {
constructor(message) {
super(message, 'EXTERNAL_SERVICE_UNAVAILABLE')
Object.defineProperty(this, 'name', { value: this.constructor.name })
}
}
@@ -0,0 +1,8 @@
{
"errors": [
{
"code": 401,
"message": "Wait, you're not supposed to be here, go through the gate as everyone else"
}
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,16 @@
import gql from 'graphql-tag'
const FB_FRAGMENT = gql`{
id
name
slug
avatar
kind
status {
status
reason
createdAt
}
}`
module.exports = FB_FRAGMENT
@@ -0,0 +1,40 @@
import gql from 'graphql-tag'
const FB_TEAM_FRAGMENT = gql`{
edges{
node{
id
name
slug
avatar
kind
status {
status
reason
createdAt
}
team {
id
name
slug
owner
profile {
avatar
banner
description
motto
updatedAt
}
members {
role
email
member {
alias
}
}
}
}
}
}`
module.exports = FB_TEAM_FRAGMENT
@@ -0,0 +1,34 @@
import gql from 'graphql-tag'
const TEAM_FB_FRAGMENT = gql`{
id
name
slug
members {
role
member {
alias
authId
}
status {
id
status
reason
createdAt
}
}
fb {
id
name
slug
avatar
kind
status {
status
reason
createdAt
}
}
}`
module.exports = TEAM_FB_FRAGMENT
@@ -0,0 +1,53 @@
import gql from 'graphql-tag'
const TEAMS_CONNECTIONS_FRAGMENT = gql`{
edges {
node {
id
name
slug
owner
status {
status
reason
createdAt
}
createdAt
profile {
avatar
banner
description
motto
updatedAt
}
members {
role
email
member {
alias
authId
}
status {
id
status
reason
createdAt
}
}
fb {
id
name
slug
avatar
kind
status {
status
reason
createdAt
}
}
}
}
}`
module.exports = TEAMS_CONNECTIONS_FRAGMENT
@@ -0,0 +1,49 @@
import gql from 'graphql-tag'
const TEAMS_FRAGMENT = gql`{
id
name
slug
owner
status {
status
reason
createdAt
}
createdAt
profile {
avatar
banner
description
motto
updatedAt
}
members {
role
email
member {
alias
authId
}
status {
id
status
reason
createdAt
}
}
fb {
id
name
slug
avatar
kind
status {
status
reason
createdAt
}
}
}`
module.exports = TEAMS_FRAGMENT
@@ -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
}
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,40 @@
import axios from 'axios'
import { logger, DatabaseError } from '../../../logger'
export const createStrategy = fbSlug => {
logger.info('createStrategy')
logger.info(fbSlug)
return new Promise((resolve, reject) => {
try {
return axios.post(process.env.PLATFORM_API_URL, {
query: `mutation strategizer_CreateStrategy($fbSlug: String!) {
strategizer_CreateStrategy(fbSlug: $fbSlug){
id
}
}`,
variables: {
fbSlug: fbSlug
}
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
logger.info('createStrategy result')
logger.info(res.data)
resolve(res.data)
})
.catch(err => {
logger.error('createStrategy err')
logger.error(err)
reject(err)
})
} catch (error) {
reject(error)
}
})
}
export default createStrategy
@@ -0,0 +1,219 @@
import axios from 'axios'
import { ApolloError } from 'apollo-server-express'
import { getUser } from '../../../middleware/getMember'
import { createStrategy } from './createStrategy'
import { notifications_sendTeamCreateConfirmation, notifications_sendTeamMemberInvite, notifications_VerifyTeamMemberInviteToken } from '../notifications/sendgrid'
import TEAMS_CONNECTIONS_FRAGMENT from '../../fragments/TeamsConnectionsFragment'
import TEAMS_FRAGMENT from '../../fragments/TeamsFragment'
import FB_FRAGMENT from '../../fragments/FBFragment'
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 notifications_sendTeamMemberInvite(email, team)
return sendInvite
},
async verifyTeamInvite(parent, { token }, ctx, info) {
let verifiedToken = null
let team = null
try {
verifiedToken = await notifications_VerifyTeamMemberInviteToken(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 = process.env.STORAGE_URL + '/module-teams/module-default/aa-avatar-default.png'
const banner = process.env.STORAGE_URL + '/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 chartsUrl = process.env.CHARTS_URL + '/AABrowserAPI/newTeam/'
logger.info(`${chartsUrl}${createTeamUrl}/${authId}`)
const createPlatformTeam = await axios.get(`${chartsUrl}${createTeamUrl}/${authId}/${process.env.AAWEB_TEAM_SHARED_SECRET}`)
.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:'Trading', 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:'Trading', 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)
})
}
let createFB;
if(createTeam){
createFB = await ctx.db.mutation.createFinancialBeings({ data:{
type:'BOT',
kind:'Indicator',
name:'Simulator ' + botName,
slug:'simulator-' + botSlug,
avatar:avatar,
team: {
connect:{
id:createTeam.id
}
}
}
}, FB_FRAGMENT)
.catch((err) => {
logger.debug(err, 'Error creating the simulator: ')
return new DatabaseError(err)
})
logger.info('createTeam creating simulator success')
logger.info(JSON.stringify(await createFB))
let createdStrategy
if(await createFB){
logger.info('createStrategy:' + 'simulator-' + botSlug)
createdStrategy = await createStrategy('simulator-' + botSlug)
logger.info('createStrategy:')
logger.info(JSON.stringify(await createStrategy))
}
}
/* **temporarily disable team creation confirmation
if(await createTeam && await createFB && await createStrategy){
notifications_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 chartsUrl = process.env.CHARTS_URL + '/AABrowserAPI/deleteTeam/'
logger.info(`${chartsUrl}${deleteTeamUrl}/${authId}`)
const deletePlatformTeam = await axios.get(`${chartsUrl}${deleteTeamUrl}/${authId}/${process.env.AAWEB_DELETE_TEAM_SHARED_SECRET}`)
.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)
})
}
}
}
@@ -0,0 +1,34 @@
import { logger } from '../../../logger'
import FB_FRAGMENT from '../../fragments/FBFragment'
import FB_TEAM_FRAGMENT from '../../fragments/FBTeamFragment'
import { isDefined, isNull } from '../../../utils'
export const fbByFbId = async (parent, { fbId }, ctx, info) => {
logger.info(`fbByFbId: ${fbId}`)
let FB = await ctx.db.query.financialBeings({where: {id: fbId}}, FB_FRAGMENT)
logger.info(`fbByFbId response: ${JSON.stringify(await FB)}`)
return FB
}
export const fbByFbSlug = async (parent, { fbSlug }, ctx, info) => {
logger.info(`fbByFbSlug: ${fbSlug}`)
let FB = await ctx.db.query.financialBeingses({where: {slug: fbSlug}, first:1}, FB_FRAGMENT)
logger.info(`fbByFbSlug response: ${JSON.stringify(await FB)}`)
return FB[0]
}
export const fbByOwner = async (parent, { fbId, fbSlug }, ctx, info) => {
logger.info('teamsByOwner')
const authId = ctx.request.headers.userid
if (!authId) {
throw new AuthenticationError()
return
}
if ((isDefined(fbId) && !isNull(fbId)) || (isDefined(fbSlug) && !isNull(fbSlug))){
return ctx.db.query.financialBeingsesConnection({where: {AND:[{team:{members_every:{member:{authId: authId}}}},{OR:[{slug:fbSlug},{id:fbId}]}]}}, FB_TEAM_FRAGMENT)
} else {
return ctx.db.query.financialBeingsesConnection({where: {team:{members_every:{member:{authId: authId}}}}}, FB_TEAM_FRAGMENT)
}
}
@@ -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}`)
})
}
@@ -0,0 +1,77 @@
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')
return ctx.db.query.teamsConnection({}, TEAMS_CONNECTIONS_FRAGMENT)
}
export const teamsByIds = async (parent, { teamIds }, ctx, info) => {
logger.info('teamsByIds')
logger.info(teamIds)
return ctx.db.query.teams({where: { id_in: teamIds } }, TEAMS_FRAGMENT)
}
export const teamById = async (parent, { teamId }, ctx, info) => {
logger.info('teamById')
logger.info(teamId)
return ctx.db.query.team({ where: { id: teamId } }, TEAMS_FRAGMENT)
}
export const teamByName = async (parent, { name }, ctx, info) => {
return ctx.db.query.team({ where: { name: name } }, TEAMS_FRAGMENT)
}
export const teamBySlug= async (parent, { slug }, ctx, info) => {
logger.info('teamsBySlug')
logger.info(`slug: ${slug}`)
return ctx.db.query.team({ where: { slug: slug } }, TEAMS_FRAGMENT)
}
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 teamAuthorization = async (parent, { teamId, role }, ctx, info) => {
const authId = ctx.request.headers.userid
if (!authId) {
throw new AuthenticationError()
return
}
if(role == undefined || role == null){
return ctx.db.query.teamsConnection({where:{ AND:{id:teamId,members_some:{member:{authId:authId}, role_in:['OWNER','ADMIN']}}}, first:1}, info)
} else {
return ctx.db.query.teamsConnection({where:{ AND:{id:teamId,members_some:{member:{authId:authId}, 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:'createdAt_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
}
@@ -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: 'createdAt_DESC'}, TEAM_FB_FRAGMENT)
logger.info('fbByTeamMember response: ', await teamMemberFB[0])
return teamMemberFB[0]
}
@@ -0,0 +1,26 @@
import { logger } from '../../../logger'
import { member, owner } from './Member'
import { teams, teamById, teamsByIds, teamByName, teamBySlug, teamWithRole, teamAuthorization, teamsByOwner, teamsByRole } from './Team'
import{ fbByTeamMember } from './TeamMember'
import{ fbByFbId, fbByFbSlug, fbByOwner } from './FinancialBeing'
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),
teamsByIds: (parent, arg, ctx, info) => teamsByIds(parent, arg, ctx, info),
teamByName: (parent, arg, ctx, info) => teamByName(parent, arg, ctx, info),
teamBySlug: (parent, arg, ctx, info) => teamBySlug(parent, arg, ctx, info),
teamWithRole: (parent, arg, ctx, info) => teamWithRole(parent, arg, ctx, info),
teamAuthorization: (parent, arg, ctx, info) => teamAuthorization(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),
fbByOwner: (parent, arg, ctx, info) => fbByOwner(parent, arg, ctx, info),
fbByFbId: (parent, arg, ctx, info) => fbByFbId(parent, arg, ctx, info),
fbByFbSlug: (parent, arg, ctx, info) => fbByFbSlug(parent, arg, ctx, info)
}
}
@@ -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)
@@ -0,0 +1,125 @@
var jwt = require('jsonwebtoken')
var axios = require('axios')
import { logger } from '../../../logger'
export const notifications_sendTeamMemberInvite = (email, team) => {
logger.info('notifications_sendTeamMemberInvite')
logger.info(email)
logger.info(teamName)
return new Promise((resolve, reject) => {
try {
return axios.post(process.env.PLATFORM_API_URL, {
query: `mutation notifications_Teams_SendTeamMemberInvite($email:String!, $teamName:String!){
notifications_Teams_SendTeamMemberInvite(email:$email, teamName:$teamName)
}`,
variables: {
email: email,
teamName: team.name
}
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
logger.info('sendTeamMemberInvite result')
logger.info(res.data)
resolve(res.data)
})
.catch(err => {
logger.info('sendTeamMemberInvite err')
logger.info(err)
reject(err)
})
} catch (error) {
reject(error)
}
})
}
export const notifications_sendTeamCreateConfirmation = (email, teamName, botName) => {
logger.info('notifications_sendTeamCreateConfirmation')
logger.info(email)
logger.info(teamName)
logger.info(botName)
return new Promise((resolve, reject) => {
try {
return axios.post(process.env.PLATFORM_API_URL, {
query: `mutation notifications_sendTeamCreateConfirmation($email: String!, $teamName: String!, $botName: String!){
notifications_Teams_SendTeamCreateConfirmation(email: $email, teamName: $teamName, botName: $botName)
}`,
variables: {
email: email,
teamName: teamName,
botName: botName
}
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
logger.info('sendTeamCreateConfirmation result')
logger.info(res.data)
resolve(res.data)
})
.catch(err => {
logger.info('sendTeamCreateConfirmation err')
logger.info(err)
reject(err)
})
} catch (error) {
reject(error)
}
})
}
export const notifications_VerifyTeamMemberInviteToken = (token) => {
logger.info('notifications_VerifyTeamMemberInviteToken')
logger.info(token)
return new Promise((resolve, reject) => {
try {
return axios.post(process.env.PLATFORM_API_URL, {
query: `mutation notifications_VerifyTeamMemberInviteToken($token: String!){
notifications_Teams_VerifyTeamMemberInviteToken(token: $token)
}`,
variables: {
token: token
}
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
logger.info('VerifyTeamMemberInviteToken result')
logger.info(res.data)
resolve(res.data)
})
.catch(err => {
logger.info('VerifyTeamMemberInviteToken err')
logger.info(err)
reject(err)
})
} catch (error) {
reject(error)
}
})
}
const verifyInviteToken = async function(token) {
const API_KEY = process.env.SG_APIKEY
let verifiedToken
try {
verifiedToken = await jwt.verify(token, API_KEY, {maxAge: '7d'})
} catch(err) {
if (err.name === "TokenExpiredError"){
throw new Error ("Error: Token Expired. Please request new invitation or apply to the team again")
} else {
throw new Error (`Error: Token error - ${err.message}`)
}
}
logger.info(`verifyInviteToken: ${verifiedToken}`)
return verifiedToken
}
@@ -0,0 +1,53 @@
# import Query.TeamConnection, Query.FinancialBeingsConnection, Team, FinancialBeings from "../generated/prisma.graphql"
directive @isAuthenticated on QUERY | FIELD | MUTATION | FIELD_DEFINITION
directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION | FIELD_DEFINITION
directive @isOwner(type: String) on QUERY | MUTATION | FIELD_DEFINITION
directive @isOwnerOrHasRole(type: String, roles: [String]) on QUERY | MUTATION | FIELD_DEFINITION
type Query {
teams: TeamConnection
teamDetails: [Team!]! @isAuthenticated
teamByName(name: String!): Team
teamBySlug(slug: String!): Team
teamById(teamId: String!): Team
teamsByIds(teamIds: [ID!]): [Team]
teamWithRole(teamId: String!, role: String!): Team
teamAuthorization(teamId: String!, role: String): Team
teamsByOwner: [Team!]! @isAuthenticated
teamsByRole: [Team!]! @isAuthenticated
fbByTeamMember: Team @isAuthenticated
fbByOwner(fbId: String, fbSlug: String): FinancialBeingsConnection @isAuthenticated
fbByFbId(fbId: String!): FinancialBeings
fbByFbSlug(fbSlug: String!): FinancialBeings
owner: Member @isAuthenticated
member: Member @isAuthenticated
}
type Mutation {
authenticate(idToken: String!): Member
verifyTeamInvite(token: String!): TeamMembers
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!, botSlug: String!): Team @isAuthenticated @isOwner(type: "Team")
sendMemberInviteSG(email: String!, teamId: String!): String
confirmMember(token: String!): String
removeTeamMember(teamId: String!, memberId: String!): String
}
fragment MemberInfo on Member {
id
authId
alias
visible
status {
...MemberStatusInfo
}
}
fragment MemberStatusInfo on MemberStatus {
status
reason
createdAt
}
+88
View File
@@ -0,0 +1,88 @@
require("@babel/register");
require("@babel/polyfill");
import express from 'express'
import createApolloServer from './ApolloServer'
import { formatError } from './errors'
import {
Prisma,
extractFragmentReplacements,
forwardTo
} from 'prisma-binding'
import { makeExecutableSchema } from 'graphql-tools'
import { importSchema } from 'graphql-import'
import parser from 'fast-xml-parser'
import axios from 'axios'
import { resolvers } from './graphql/resolvers'
import { schemaDirectives } from './directives'
import pubsub from './pubsub'
import { logger } from './logger'
import wrongPreshared from './errors/notAllowed.json'
const GRAPHQL_ENDPOINT = '/graphql'
const GRAPHQL_SUBSCRIPTIONS = '/graphql'
const PORT = process.env.PORT
const NODE_ENV = process.env.NODE_ENV
const SCHEMA_PATH = process.env.SCHEMA_PATH
const db = new Prisma({
fragmentReplacements: extractFragmentReplacements(resolvers),
typeDefs: importSchema(SCHEMA_PATH),
endpoint: process.env.PRISMA_ENDPOINT,
secret: process.env.PRISMA_SECRET,
debug: false
})
const app = express()
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)
const corsOptions = {
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}
const options = {
port: 4001,
cors: corsOptions,
endpoint: '/graphql',
subscriptions: '/graphql',
playground: '/graphiql'
}
const server = createApolloServer(app, {
graphqlEndpoint: GRAPHQL_ENDPOINT,
subscriptionsEndpoint: GRAPHQL_SUBSCRIPTIONS,
apolloServerOptions: { formatError },
typeDefs: importSchema('src/graphql/schema.graphql'),
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
schemaDirectives,
context: req => ({
...req ,
token: req.headers ? req.headers : undefined,
user: req.user ? req.user : undefined,
userId: (req.headers && req.headers.userid) ? req.headers.userid : undefined,
db,
pubsub
})
})
server.listen({ port: PORT }, () => {
logger.info(
`🚀 GraphQL Server is running on http://localhost:${PORT}${GRAPHQL_ENDPOINT} in "${NODE_ENV}" mode\n`
)
})
@@ -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
}
@@ -0,0 +1,33 @@
import { createLogger, format, transports } from 'winston'
import { inspect } from 'util'
import { AuthenticationError, ApolloError, DatabaseError, WrongArgumentsError, ServiceUnavailableError, ConflictError } from './errors'
class LoggerService {
constructor() {
const winstonTransports = [
new transports.Console({
format: format.combine(
format.colorize(),
format.timestamp({ format: 'YY/MM/DD HH:mm' }),
format.printf(
info => `${info.timestamp} - ${info.level}: ${JSON.parse(JSON.stringify(inspect(info.message)))}`
)
),
}),
]
const logger = createLogger({
level: 'info',
transports: winstonTransports,
exceptionHandlers: winstonTransports,
})
return new Proxy(this, {
get: (target, propKey) => logger[propKey],
})
}
}
export const logger = new LoggerService() // Default logger
export { LoggerService, AuthenticationError, ApolloError, ConflictError, DatabaseError, WrongArgumentsError, ServiceUnavailableError }
@@ -0,0 +1,56 @@
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)
return new Promise((resolve, reject) => {
try {
return axios.post(process.env.PLATFORM_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
@@ -0,0 +1,3 @@
import moduleAuth from './moduleAuth'
export { moduleAuth }
@@ -0,0 +1,24 @@
const jwt = require('express-jwt')
const jwksRsa = require('jwks-rsa')
// Authentication middleware. When used, the
// if the access token exists, it be verified against
// the Auth0 JSON Web Key Set
const checkJwt = jwt({
// Dynamically provide a signing key
// based on the kid in the header and
// the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 1,
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
// Validate the audience and the issuer.
credentialsRequired: false,
audience: process.env.AUTH0_AUDIENCE,
issuer: process.env.AUTH0_ISSUER,
algorithms: [`RS256`]
})
module.exports = checkJwt
@@ -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
@@ -0,0 +1,3 @@
import { PubSub } from 'graphql-subscriptions'
export default new PubSub()
@@ -0,0 +1,191 @@
const Azure = require("@azure/storage-blob")
const azureAccount = process.env.AZURE_STORAGE_ACCOUNT
const azureKey = process.env.AZURE_STORAGE_ACCESS_KEY
const azureStorageUrl = process.env.AZURE_STORAGE_URL
/* Create Azure Storage pipeline
* @accountName: Azure Blob Storage account name (required) - Environment variable
* @accountKey: Azure Blob Storage storage key (required) - Environment variable
*/
const createStoragePipline = async (accountName, accountKey) => {
return Azure.StorageURL.newPipeline(
new Azure.SharedKeyCredential(accountName, accountKey)
)
}
/* Create Azure Storage Service URL
* @storageURL: URL for Azure Storage endpoint (required) - Environment variable
* @pipeline: Azure Storage Pipline (required) - created using createStoragePipeline()
*/
const createServiceURL = async (storageURL, pipeline) => {
return new Azure.ServiceURL(
storageURL,
pipeline
)
}
/* List containers
* @pipeline: Azure Storage Pipline (required) - created using createServiceURL
* @service_url: URL for Azure Storage endpoint (required) - Environment variable
*/
const listContainers = async (serviceURL) => {
let marker;
do {
const listContainersResponse = await serviceURL.listContainersSegment(
Azure.Aborter.None,
marker,
);
marker = listContainersResponse.marker;
for (const container of listContainersResponse.containerItems) {
console.log(`Container: ${container.name}`);
}
} while (marker);
}
/* Create containers
* @serviceURL: URL for Azure Storage endpoint (required) - created using createServiceURL
* @containerName: Container Name
* Returns: containerURL
*/
const createContainer = async (serviceURL, containerName) => {
const containerURL = ContainerURL.fromServiceURL(serviceURL, containerName)
const createContainerResponse = await containerURL.create(Aborter.None)
console.log(
`Created container ${containerName} successfully`,
await createContainerResponse.requestId
)
return containerURL
}
/* Create containers
* @containerURL: continer URL (required) - created using createContainer
* @blobName: Blob Name
* Returns: blobURL
*/
const createBlob = async (containerURL, blobName) => {
const blobURL = Azure.BlobURL.fromContainerURL(containerURL, blobName)
console.log(`created blob successfully`, await blobURL)
return blobURL
}
/* Upload Blob
* @blobURL: blobURL (required) - created using createBlob
* @blobContent: Blob Content
* Returns: uploadBlobResponse
*/
const uploadBlob = async (blobURL, blobContent) => {
const blockBlobURL = Azure.BlockBlobURL.fromBlobURL(blobURL)
const uploadBlobResponse = await blockBlobURL.upload(
Azure.Aborter.None,
blobContent,
blobContent.length
);
console.log(`Upload block blob ${blobName} successfully`, await uploadBlobResponse)
return uploadBlobResponse
}
/* Download Blob
* @blobURL: blobURL (required) - created using createBlob
* @blobContent: Blob Content
* Returns: downloadBlockBlobResponse
*/
const downloadBlob = async (blobURL, blobContent) => {
const downloadBlockBlobResponse = await blobURL.download(Azure.Aborter.None, 0)
console.log(
"Downloaded blob content",
downloadBlockBlobResponse.readableStreamBody.read(content.length).toString()
)
console.log(`BlockBlobResponse: ${downloadBlockBlobResponse}`)
return downloadBlockBlobResponse
}
/* Delete Blob
* @blobURL: blobURL (required) - created using createBlob
* Returns: bool
*/
const deleteBlob = async (blobrURL) => {
await blobrURL.delete(Azure.Aborter.None);
console.log("deleted blob", blobrURL);
return true
}
/* Delete Container
* @containerURL: continer URL (required) - created using createContainer
* Returns: bool
*/
const deleteContainer = async (containerURL) => {
await containerURL.delete(Azure.Aborter.None);
console.log("deleted container", containerURL);
return true
}
const createSASQueryURL = async (containerName) => {
let today = new Date()
let week = new Date()
week.setDate(today.getDate() + 7)
// Create SharedKeyCredential and attach to pipline
const SKC = new Azure.SharedKeyCredential(azureAccount, azureKey)
const pipeline = Azure.StorageURL.newPipeline(SKC)
// Create container URL
const serviceURL = new Azure.ServiceURL(azureStorageUrl, pipeline)
const containerURL = Azure.ContainerURL.fromServiceURL(serviceURL, containerName)
//list container and check if already exists.
let marker
let containerCheck = null
do {
const listContainersResponse = await serviceURL.listContainersSegment(
Azure.Aborter.None,
marker,
)
// console.log(`ContainerCheck marker: `, listContainersResponse)
marker = listContainersResponse.marker;
for (const container of listContainersResponse.containerItems) {
console.log(`ContainerCheck: ${container.name} | ${containerName} | ${marker}`)
if(container.name === containerName){
containerCheck = container.name
}
}
} while (marker)
// if container doesn't exist, create one
let newContainer
if(containerCheck === null){
newContainer = await containerURL.create(Azure.Aborter.None, { access: 'blob' })
}
console.log('createSASQueryURL container: ', containerURL, containerCheck, newContainer)
// Set permissions for service, resource types and containers
const SASContainerPerms = await Azure.AccountSASPermissions.parse('rwlacu')
const SASServicePerms = await Azure.AccountSASServices.parse('b')
const SASResourceTypes = await Azure.AccountSASResourceTypes.parse('co')
console.log('createSASQueryURL permissions: ', SASServicePerms, SASResourceTypes, SASContainerPerms)
// Generate SAS url
const SASQueryParameters = await Azure.generateBlobSASQueryParameters(
{
version: '2017-11-09',
protocol: 'https,http',
expiryTime: week,
permissions: 'rwlac',
containerName: containerName
}, SKC )
console.log('createSASQueryURL SASQueryParameters: ', containerURL.url, SASQueryParameters)
return SASQueryParameters
}
module.exports = {
createStoragePipline,
createServiceURL,
listContainers,
createContainer,
createBlob,
uploadBlob,
downloadBlob,
deleteBlob,
deleteContainer,
createSASQueryURL
}
@@ -0,0 +1,69 @@
export const toLocalTime = (epoch) => {
const localDate = new Date(0);
localDate.setUTCSeconds(epoch);
const options = { year: 'numeric', month: 'long', day: '2-digit' };
return (
`${localDate.toLocaleDateString(options)
} at ${
localDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}`
);
};
export const temporaryFunction = input => input;
export function isDefined (d) {
return d !== null && typeof d !== 'undefined'
}
export function isNull (d) {
return d === null
}
export const isArray = Array.isArray
export function isObject (d) {
return isDefined(d) && typeof d === 'object' && !Array.isArray(d)
}
export function isEmpty (obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) return false
}
return true
}
export function validObject (object, ...keys) {
return keys.reduce((a, b) => (a || { })[b], object) !== undefined
}
export function mergeDeep (target, ...sources) {
if (!sources.length) return target
const source = sources.shift()
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} })
mergeDeep(target[key], source[key])
} else {
Object.assign(target, { [key]: source[key] })
}
}
}
return mergeDeep(target, ...sources)
}
export function slugify (string) {
const a = 'àáäâãåèéëêìíïîòóöôùúüûñçßÿœæŕśńṕẃǵǹḿǘẍźḧ·/_,:;'
const b = 'aaaaaaeeeeiiiioooouuuuncsyoarsnpwgnmuxzh------'
const p = new RegExp(a.split('').join('|'), 'g')
return string.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, '-and-') // Replace & with 'and'
.replace(/[^\w-]+/g, '') // Remove all non-word characters
.replace(/--+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, '') // Trim - from end of text
}