Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions api/directives/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import gql from 'graphql-tag'
import { defaultFieldResolver } from 'graphql'
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
import { GqlAuthorizationError } from '@/lib/error'

const DIRECTIVE_NAME = 'auth'

export const typeDef = gql`
directive @${DIRECTIVE_NAME}(allow: [Role!]!) on FIELD_DEFINITION
enum Role {
ADMIN
OWNER
USER
}
`

export function apply (schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
const upperDirective = getDirective(schema, fieldConfig, DIRECTIVE_NAME)?.[0]
if (upperDirective) {
const { resolve = defaultFieldResolver } = fieldConfig
const { allow } = upperDirective
return {
...fieldConfig,
resolve: async function (parent, args, context, info) {
checkFieldPermissions(allow, parent, args, context, info)
return await resolve(parent, args, context, info)
}
}
}
}
})
}

function checkFieldPermissions (allow, parent, args, { me }, { parentType }) {
// TODO: should admin users always have access to all fields?

if (allow.indexOf('OWNER') >= 0) {
if (!me) {
throw new GqlAuthorizationError('you must be logged in to access this field')
}

switch (parentType.name) {
case 'User':
if (me.id !== parent.id) {
throw new GqlAuthorizationError('you must be the owner to access this field')
}
break
default:
// we could just try the userId column and not care about the type
// but we want to be explicit and throw on unexpected types instead
// to catch potential issues in our authorization layer fast
throw new GqlAuthorizationError('failed to check owner: unknown type')
}
}
}
14 changes: 14 additions & 0 deletions api/directives/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { makeExecutableSchema } from '@graphql-tools/schema'

import * as upper from './upper'
import * as auth from './auth'

const DIRECTIVES = [upper, auth]

export function makeExecutableSchemaWithDirectives (typeDefs, resolvers) {
const schema = makeExecutableSchema({
typeDefs: [...typeDefs, ...DIRECTIVES.map(({ typeDef }) => typeDef)],
resolvers
})
return DIRECTIVES.reduce((acc, directive) => directive.apply(acc), schema)
}
30 changes: 30 additions & 0 deletions api/directives/upper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import gql from 'graphql-tag'
import { defaultFieldResolver } from 'graphql'
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'

/** Example schema directive that uppercases the value of the field before returning it to the client */

const DIRECTIVE_NAME = 'upper'

export const typeDef = gql`directive @${DIRECTIVE_NAME} on FIELD_DEFINITION`

export function apply (schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
const upperDirective = getDirective(schema, fieldConfig, DIRECTIVE_NAME)?.[0]
if (upperDirective) {
const { resolve = defaultFieldResolver } = fieldConfig
return {
...fieldConfig,
resolve: async function (parent, args, context, info) {
const result = await resolve(parent, args, context, info)
if (typeof result === 'string') {
return result.toUpperCase()
}
return result
}
}
}
}
})
}
4 changes: 0 additions & 4 deletions api/resolvers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,10 +936,6 @@ export default {

User: {
privates: async (user, args, { me, models }) => {
if (!me || me.id !== user.id) {
return null
}

return user
},
optional: user => user,
Expand Down
9 changes: 2 additions & 7 deletions api/ssrApollo.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { SchemaLink } from '@apollo/client/link/schema'
import { makeExecutableSchema } from '@graphql-tools/schema'
import resolvers from './resolvers'
import typeDefs from './typeDefs'
import { schema } from '@/pages/api/graphql'
import models from './models'
import { print } from 'graphql'
import lnd from './lnd'
Expand All @@ -22,10 +20,7 @@ export default async function getSSRApolloClient ({ req, res, me = null }) {
const client = new ApolloClient({
ssrMode: true,
link: new SchemaLink({
schema: makeExecutableSchema({
typeDefs,
resolvers
}),
schema,
context: {
models,
me: session
Expand Down
2 changes: 1 addition & 1 deletion api/typeDefs/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default gql`
proportion: Float

optional: UserOptional!
privates: UserPrivates
privates: UserPrivates @auth(allow: [OWNER])

meMute: Boolean!
meSubscriptionPosts: Boolean!
Expand Down
15 changes: 10 additions & 5 deletions pages/api/graphql.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { ApolloServer } from '@apollo/server'
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'
import { startServerAndCreateNextHandler } from '@as-integrations/next'
import resolvers from '@/api/resolvers'
import models from '@/api/models'
import lnd from '@/api/lnd'
import typeDefs from '@/api/typeDefs'
import { makeExecutableSchemaWithDirectives } from '@/api/directives'
import resolvers from '@/api/resolvers'
import { getServerSession } from 'next-auth/next'
import { getAuthOptions } from './auth/[...nextauth]'
import search from '@/api/search'
import { multiAuthMiddleware } from '@/lib/auth'
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'

export const schema = makeExecutableSchemaWithDirectives(typeDefs, resolvers)

const apolloServer = new ApolloServer({
typeDefs,
resolvers,
schema,
introspection: true,
allowBatchedHttpRequests: true,
plugins: [{
Expand Down Expand Up @@ -42,7 +44,10 @@ const apolloServer = new ApolloServer({
}
}
}
}, ApolloServerPluginLandingPageDisabled()]
},
process.env.NODE_ENV === 'development' && ApolloServerPluginLandingPageLocalDefault(
{ embed: { endpointIsEditable: false, persistExplorerState: true, displayOptions: { theme: 'dark' } }, footer: false })
].filter(Boolean)
})

export default startServerAndCreateNextHandler(apolloServer, {
Expand Down