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

+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
}