Restructure api
This commit is contained in:
1 parent
133fc67707
commit
856d64cb6e
17 files changed
+9945
No files matched your search
@@ -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