Restructure api
This commit is contained in:
1 parent
133fc67707
commit
856d64cb6e
17 files changed
+9945
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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...
|
||||
@@ -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!
|
||||
}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
Generated
+9133
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,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
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user