Restructure api

This commit is contained in:
Barry Low committed 2018-09-11 12:06:59 -07:00
1 parent 133fc67707
commit 856d64cb6e
17 files changed
+9945

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
PRISMA_SECRET=prisma-secret
PRISMA_ENDPOINT=http://localhost:4466
AUTH0_DOMAIN=auth0-domain
AUTH0_AUDIENCE=auth0-audience
AUTH0_ISSUER=auth0-issuer
NODE_ENV=development
GENERATE_SOURCEMAP=false
COMPOSE_HTTP_TIMEOUT=2000
VIRTUAL_HOST=teams.advancedalgos.net
LETSENCRYPT_HOST=teams.advancedalgos.net
LETSENCRYPT_EMAIL=support@advancedalgos.net
VIRTUAL_PORT=4000
+11
View File
@@ -0,0 +1,11 @@
/.env
/database/.env
/database/docker-compose.prisma.prod.yml
/docker-compose.prisma.prod.yml
/docker-compose.prod.yml
.graphqlconfig.prod.yml
dist
node_modules
.idea
.vscode
*.log
+12
View File
@@ -0,0 +1,12 @@
projects:
app:
schemaPath: src/schema.graphql
extensions:
endpoints:
dev1:
url: http://0.0.0.0:4000/graphql
database:
schemaPath: src/generated/prisma.graphql
extensions:
prisma: database/prisma.yml
+21
View File
@@ -0,0 +1,21 @@
FROM mhart/alpine-node:8
ENV PORT=4000
RUN mkdir -p /var/www/app && \
mkdir -p /var/www/app/node_modules
# Use app directory as current workdir
WORKDIR /var/www/app
ENV PATH /var/www/app/node_modules/.bin:$PATH
# Copy package.json to workdir
COPY package.json /var/www/app/package.json
# Install dependencies
RUN npm install
# Copy application code
COPY . /var/www/app
# Expose Port
EXPOSE $PORT
+114
View File
@@ -0,0 +1,114 @@
# Advance Algos Team GraphQL API Server
This is the Advanced Algos Team Module server-side GraphQL API server. We use Webpack 4 + Docker to develop locally, then build to a distribution bundle that can then be uploaded via FTP, git or used with a continuous integration workflow.
This modules client-side component is the [aateam-client package](../aateam-client#README).
For more information on the general architecture and related technology of this module, please visit this repos [main documenation](../../docs#README).
## Getting Started
Run
```
npm install
```
then
```
npm run dev
```
### Using Docker
Get latest Docker and Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/)
#### Using Docker container for Development
To run starter kit in development mode with hot code reload execute:
Run docker container in background:
```
docker-compose up -d
```
If image issues, try running:
```
docker-compose up --build
```
then run
```
docker-compose up -d
```
To view logs
```
docker-compose logs -f -t
```
Hit Ctrl + c to exit logs without stopping service
To stop service
```
docker-compose down
```
## Overview
The development architecture of the Algobots Community site is to allow both rapid initial development and ongoing rapid-evolution. As with any opinionated workflow, there's a learning curve, but the libraries and architecture of this application have been chosen to maximize collaboration, flexibility, and extensibility.
### Main Libraries
- [*Webpack 4*](https://webpack.js.org/):web application bundler. Allows us to us latest newer JS ECMA features that increase productivity and code clarity. Also allows additional libraries such as modular and programmable CSS via SASS/SCSS/LESS. Provides webserver for local development.
- [*Bulma CSS Framework*](https://bulma.io/documentation/): Bulma is a highly-customizable CSS-only framework allowing us to quickly prototype the layout of a webapp without being complicated by opinionated functionality represented in libraries such as Bootstrap or Material UI. Grants us the flexibility to create a custom look at top speed whilst minimizing baggage.
- [*HyperApp*](https://github.com/hyperapp/hyperapp): Allows a controlled Flux-styled approach to state-management and data-flow through components like React minus the excess boilerplate and learning curve.
- [*Docker*](https://www.docker.com/what-docker)
### File organization
```
. # Top level directory located at your choice
├── src
│ ├── actions # Actions are functions that change the app state
│ ├── assets # images, logos and other static assets
│ ├── state # Schema of state — the single source of truth concerning app state.
│ ├── styles # SASS/SCSS styles
│ ├── utils # Local storage and other utils
│ └── views # Main folder for editing site content
│ ├─ nav # Nav components — header, footer, etc.
│ ├─ pages # Nav components — header, footer, etc.
│ │ ├─ landing # Homepage
│ │ │ ├─ sections # Homepage sections. Collated in its own index.js
│ │ │ └─ index.js # Main homepage component. Pulls in sections
│ │ └─ ... # Other page/component folders
│ └─ index.js # Main view container. Main router
├── html
│ └── index.html # index.html template pulled in by Webpack to attach transpiled bundles
├── index.js # JS entry point to website
│
├── tools # utility scripts, mostly for webpack configuration
│
├── node_modules # created by running `npm install` in this packages root dir
├── dist # created by `npm run develop` - a cache of bundled files served by Webpack for local development
└── build # created by `npm run build` - location of deployable production bundle
```
## Deployment
The simplest deployment is compiling/transpiling the site to a distribution bundle and then uploading the files in the bundle to a web-accessible folder or storage.
For this application we use Webpack to transpile the application which combines and minimizes all code and assets into an optimized bundle without and of the development code and resources.
To build, run:
```
npm run build
```
Looking in this packages directory, there should be a ./build directory with the final built files that can be uploaded.
### Create Docker image
```
docker build -t aateamapi/prod .
```
### Deployment instructions
Coming soon...
+94
View File
@@ -0,0 +1,94 @@
enum Role {
OWNER
ADMIN
MEMBER
}
enum Status {
ACTIVE
INACTIVE
APPLY
INVITE
}
enum FBType {
BOT
NET
ADVANCED
}
enum FBKind {
TRADER
INDICATOR
EXTRACTOR
PLOTTER
}
type Team {
id: ID! @unique
name: String! @unique
slug: String! @unique
profile: TeamProfile
members: [TeamMembers!]! @relation (name: "TeamMembers", onDelete: SET_NULL)
wallet: Wallet
fb: [FinancialBeings!]! @relation (name: "TeamFinancialBeing", onDelete: SET_NULL)
status: [TeamStatus!]!
createdAt: DateTime!
}
type TeamProfile {
description: String
motto: String
avatar: String
updatedAt: DateTime!
}
type TeamStatus {
status: Status @Default(value: inactive)
reason: String
createdAt: DateTime!
}
type TeamMembers {
member: Member @relation (name: "MemberOnTeam", onDelete: SET_NULL)
team: Team @relation (name: "TeamMembers", onDelete: SET_NULL)
role: Role @default(value: MEMBER)
status: [MemberStatus!]!
updatedAt: DateTime!
}
type Member {
id: ID! @unique
nickname: String
team: TeamMembers @relation (name: "MemberOnTeam", onDelete: SET_NULL)
authId: String! @unique
visible: Boolean! @default(value: true)
createdAt: DateTime!
}
type MemberStatus {
status: Status @Default(value: inactive)
reason: String
createdAt: DateTime!
}
type Wallet {
balance: Float
updatedAt: DateTime!
}
type FinancialBeings {
id: ID! @unique
type: FBType
kind: FBKind
team: Team @relation (name: "TeamFinancialBeing", onDelete: SET_NULL)
status: [FBStatus!]!
updatedAt: DateTime!
createdAt: DateTime!
}
type FBStatus {
status: Status @Default(value: inactive)
reason: String
createdAt: DateTime!
}
+8
View File
@@ -0,0 +1,8 @@
secret: ${env:PRISMA_SECRET}
endpoint: ${env:PRISMA_ENDPOINT}
datamodel: datamodel.graphql
hooks:
post-deploy:
- graphql get-schema --project database
- graphql prepare
+29
View File
@@ -0,0 +1,29 @@
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.15.1
restart: always
ports:
- "4466:4466"
environment:
PRISMA_CONFIG: |
managementApiSecret: ${PRISMA_SECRET}
port: 4466
databases:
default:
connector: postgres
host: postgres
port: 5432
user: prisma
password: prisma
migrations: true
postgres:
image: postgres
restart: always
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
+50
View File
@@ -0,0 +1,50 @@
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.15.1
depends_on:
- postgres
restart: always
ports:
- "4466:4466"
environment:
PRISMA_CONFIG: |
managementApiSecret: ${PRISMA_SECRET}
port: 4466
databases:
default:
connector: postgres
host: postgres
port: 5432
user: prisma
password: prisma
migrations: true
postgres:
image: postgres
restart: always
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
volumes:
- postgres:/var/lib/postgresql/data
node:
image: aateamapi-dev:v1
build:
context: .
dockerfile: ./Dockerfile
depends_on:
- prisma
env_file: ./.env
container_name: aateamapi
tty: true
stdin_open: true
volumes:
- ./:/var/www/app
- /var/www/app/node_modules
working_dir: /var/www/app
restart: always
command: node -r dotenv/config src/index.js
ports:
- "4000:4000"
volumes:
postgres:
+9133
View File
File diff suppressed because it is too large. Load diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "aa-team-api",
"scripts": {
"start": "nodemon -e js,graphql -x node -r dotenv/config src/index.js",
"debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js",
"playground": "graphql playground --dotenv dotenv/config",
"dev": "npm-run-all --parallel debug playground"
},
"dependencies": {
"cors": "^2.8.4",
"dotenv": "^6.0.0",
"express-jwt": "^5.3.1",
"graphql-import": "^0.6.0",
"graphql-tools": "3.1.1",
"graphql-yoga": "^1.16.1",
"jsonwebtoken": "^8.3.0",
"jwks-rsa": "^1.3.0",
"lodash.get": "^4.4.2",
"nodemon": "^1.18.3",
"prisma-binding": "2.1.4"
},
"devDependencies": {
"graphql-cli": "^2.16.5",
"npm-run-all": "4.1.3",
"prisma": "^1.14.0"
}
}
+117
View File
@@ -0,0 +1,117 @@
const _get = require('lodash.get')
const validateAndParseIdToken = require('./helpers/validateAndParseIdToken')
const createMember = require('./index')
const memberLocationOnContext = 'request.member'
const bearerAccessToken = 'request.headers.authorization'
const ctxMember = ctx => _get(ctx, memberLocationOnContext)
const ctxToken = ctx => _get(ctx, bearerAccessToken)
const isLoggedIn = async ctx => {
let member = ctxMember(ctx, memberLocationOnContext)
let token = ctxToken(ctx, bearerAccessToken)
let memberToken
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 validateAndParseIdToken(token)
const auth0id = memberToken.sub
member = await ctx.db.query.member({ where: { auth0id } })
if (!member) {
member = await createMember(ctx, memberToken)
}
} catch (err) {
memberToken = false
}
}
}
if (!member) {
throw new Error(`Not logged in`)
}
return member
}
const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, typeId }) =>
ctx.db.exists[type]({ id: typeId, owner: { auth0id: memberId } })
const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ auth0id: memberId })
const directiveResolvers = {
isAuthenticated: async (next, source, args, ctx) => {
let result = await isLoggedIn(ctx)
console.log('directive isAuthenticated: ', result)
return next(result)
},
hasRole: (next, source, { roles }, ctx) => {
const { role } = isLoggedIn(ctx)
console.log('directive hasRole: ', role)
if (roles.includes(role)) {
return next()
}
throw new Error(`Unauthorized, incorrect role`)
},
isOwner: async (next, source, { type }, ctx) => {
const { id: typeId } =
source && source.id
? source
: ctx.request.body.variables ? ctx.request.body.variables : { id: null }
const { auth0id: memberId } = await isLoggedIn(ctx)
console.log('directive isOwner 0: ', type, typeId, memberId)
const isOwner =
type === `Member`
? memberId === typeId
: await isRequestingMemberAlsoOwner({ ctx, memberId, type, typeId })
console.log('directive isOwner 1: ', source, isOwner, type, typeId, memberId)
if (isOwner) {
return next()
}
throw new Error(`Unauthorized, must be owner`)
},
isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => {
const { auth0id: memberId } = await isLoggedIn(ctx)
console.log('directive isOwnerOrHasRole 1: ', memberId, roles, type)
if(memberId === undefined && role=== undefined) throw new Error(`Not logged in`)
if (roles.includes(role)) {
return next()
}
const { auth0id: typeId } = ctx.request.body.variables
const isOwner = await isRequestingMemberAlsoOwner({
ctx,
memberId,
type,
typeId
})
console.log('directive isOwnerOrHasRole 2: ', typeId, memberId, role, isOwner)
if (isOwner) {
return next()
}
const hasRole = await isRequestingMember({
ctx,
memberId,
type,
typeId
})
if(hasRole){
return next()
}
throw new Error(`Unauthorized, not owner or incorrect role`)
}
}
module.exports = { directiveResolvers }
+10
View File
@@ -0,0 +1,10 @@
const getMember = async (req, res, next, db) => {
if (!req.user) return next()
const member = await db.query.member({
where: { auth0id: req.user.sub }
})
req.user = { token: req.user, ...member }
next()
}
module.exports = { getMember }
+24
View File
@@ -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 }
+28
View File
@@ -0,0 +1,28 @@
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 validateParseIdToken = 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 = validateParseIdToken
+218
View File
@@ -0,0 +1,218 @@
const { GraphQLServer } = require('graphql-yoga')
const {
Prisma,
extractFragmentReplacements,
forwardTo
} = require('prisma-binding')
const { makeExecutableSchema } = require('graphql-tools')
const { importSchema } = require('graphql-import')
const { checkJwt } = require('./auth/middleware/jwt')
const { getMember } = require('./auth/middleware/getMember')
const validateParseIdToken = require('./auth/helpers/validate-parse-id-token')
const { directiveResolvers } = require('./auth-directives')
const ctxMember = ctx => ctx.request.member
const resolvers = {
Query: {
async member(parent, { auth0id }, ctx, info) {
return ctx.db.query.member({ where: { auth0id } }, info)
},
teams(parent, args, ctx, info) {
return ctx.db.query.teamsConnection({},
`{
edges {
node {
id
name
slug
profile {
avatar
description
motto
}
owner {
nickname
visible
profile {
avatar
}
}
members {
role
member {
nickname
visible
profile {
avatar
}
}
}
}
}
}`)
},
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 teamByOwner(parent, { ownerId }, ctx, info) {
return ctx.db.query.teamsConnection({where: { owner: { auth0id: ownerId }}},
`{
edges {
node {
id
name
slug
profile {
avatar
description
motto
}
owner {
auth0id
nickname
visible
profile {
avatar
}
}
members {
role
member {
nickname
visible
profile {
avatar
}
}
}
}
}
}`)
},
async owner(parent, args, ctx, info) {
console.log('resolver.query.owner ctx: ', ctxMember(ctx))
return ctx.db.query.member({ where: { id: ctxMember(ctx).auth0id } }, info)
.catch((res) => {
console.log('createTeam error: ', res)
const errors = res.graphQLErrors.map((error) => {
return error.message
})
})
},
members: forwardTo('db')
},
Mutation: {
async authenticate(parent, { idToken }, ctx, info) {
let memberToken = null
try {
memberToken = await validateAndParseIdToken(idToken)
console.log('authenticate.memberToken: ', await memberToken)
} catch (err) {
console.log('authenticat.validateAndParseIdToken err: ', err)
throw new Error(err.message)
}
const auth0id = memberToken.sub
let member = await ctx.db.query.member({ where: { auth0id } }, info)
if (!member) {
try {
return createMember(ctx, info, memberToken)
} catch (error) {
throw new Error(error.message)
}
}
}
return member
},
async createTeam(parent, { name, slug, owner }, ctx, info) {
return ctx.db.mutation.createTeam({
data: {
name: name,
slug: slug,
owner: {connect:{auth0id: owner}},
members: {create: {role: 'OWNER', member: {connect: {auth0id: owner}}}}
}
}, info)
.catch((res) => {
console.log('createTeam error: ', res)
const errors = res.graphQLErrors.map((error) => {
return error.message
})
})
},
async deleteTeam(parent, { id }, ctx, info) {
return ctx.db.mutation.deleteTeam({ where: { id } }, info)
.catch((res) => {
console.log('deleteTeam error: ', res)
const errors = res.graphQLErrors.map((error) => {
return error.message
})
})
}
}
}
const db = new Prisma({
fragmentReplacements: extractFragmentReplacements(resolvers),
typeDefs: 'src/generated/prisma.graphql',
endpoint: process.env.PRISMA_ENDPOINT,
secret: process.env.PRISMA_SECRET,
debug: true
})
const schema = makeExecutableSchema({
typeDefs: importSchema('src/schema.graphql'),
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
directiveResolvers
})
const server = new GraphQLServer({
schema,
context: req => ({
...req,
db
})
})
server.express.post(
server.options.endpoint,
checkJwt,
(err, req, res, next) => {
if (err) return res.status(401).send(err.message)
next()
}
)
server.express.post(server.options.endpoint, (req, res, done) =>
getMember(req, res, done, db)
)
const whitelist = [
'http://localhost:1337',
'http://0.0.0.0:5000'
]
const corsOptionsDelegate = (req, callback) => {
var corsOptions
if (whitelist.indexOf(req.header('Origin')) !== -1) {
corsOptions = { origin: true, credentials: true } // reflect (enable) the requested origin in the CORS response
} else {
corsOptions = { origin: false, credentials: true } // disable CORS for this request
}
callback(null, corsOptions) // callback expects two parameters: error and options
}
const options = {
cors: corsOptionsDelegate,
endpoint: '/graphql',
subscriptions: '/graphql',
playground: '/playground'
}
server.start(options, () => console.log(`Server is running on http://localhost:4000${server.options.endpoint}`))
module.exports= { createPrismaMember: createPrismaMember }
+35
View File
@@ -0,0 +1,35 @@
# import Query.TeamConnection, Team from "./generated/prisma.graphql"
directive @isAuthenticated on QUERY | FIELD | MUTATION
directive @hasRole(roles: [String]) on QUERY | FIELD | MUTATION
directive @isOwner(type: String) on QUERY | MUTATION
directive @isOwnerOrHasRole(type: String, roles: [String]) on QUERY | MUTATION
type Query {
teams: TeamConnection
teamDetails: [Team!]! @isAuthenticated
teamByName(name: String!): Team
teamById(teamId: String!): Team
teamByOwner(ownerId: String!): TeamConnection
owner: Member @isAuthenticated
member(auth0id: String!): Member @isAuthenticated
}
type Mutation {
authenticate(idToken: String!): Member
createTeam(name: String!, slug: String, owner: String): Team @isAuthenticated
deleteTeam(id: ID!): Team @isAuthenticated @isOwner(type: "Team")
updateProfile(data: MemberProfileInput): Member @isAuthenticated
}
input MemberProfileInput {
auth0id: String!
nickname: String!
anonymous: Boolean
visible: Boolean
avatar: String
email: String
repo: String
bio: String
status: String
}