Passing request into Apollo Server context generated in function - javascript

Learning GraphQL and a bit stuck with passing req into a generate context function I made to keep things neat.
I think I am doing something dumb with the line createContext((req) => req) as if I console.log( ctx.request ) in a route handler I get [Function (anonymous)]
Whats an alternative way to capture the this.req from the server.ts scope and pass it into createContext?
server.ts
import { ApolloServer } from 'apollo-server'
import { schema } from './nexusSchema'
import { createContext } from './context'
const server = new ApolloServer({
schema,
context: createContext((req) => req),
})
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
context.ts
import { PrismaClient, Prisma as PrismaTypes } from '#prisma/client'
import { PrismaDelete, onDeleteArgs } from '#paljs/plugins'
import { PubSub } from 'apollo-server'
class Prisma extends PrismaClient {
constructor(options?: PrismaTypes.PrismaClientOptions) {
super(options)
}
async onDelete(args: onDeleteArgs) {
const prismaDelete = new PrismaDelete(this)
await prismaDelete.onDelete(args)
}
}
const prisma = new Prisma()
const pubsub = new PubSub()
export interface Context {
prisma: Prisma
select: any
pubsub: PubSub
request: {
request: {
headers: {
authorization: string
}
}
connection: {
context: {
Authorization: string
}
}
}
}
export function createContext(req): Context {
return {
prisma,
select: {},
pubsub,
request: req,
}
}

I was being dumb after all.
Note, I also had to adjust the interface for the request object to suit Apollo Server 2!
server.ts
const server = new ApolloServer({
schema,
context: createContext,
})
context.ts
export interface Context {
prisma: Prisma
select: any
pubsub: PubSub
request: {
req: {
headers: {
authorization: string
}
}
connection: {
context: {
Authorization: string
}
}
}
}
export const createContext = (req): Context => {
return {
prisma,
select: {},
pubsub,
request: req,
}
}

Related

WebSocket connection to 'ws://localhost:4000/graphql' failed:

I am getting this Websocket failed to Connect error for both client and server side now (as shown in the image below). I am not using any other Websocket configuration other than the one specified in the apollo client. This has been baffling me for about 2 days. Any help would be appreciated. Let me know if you need to see any further code.
I have a Vue app client that connects to graphql apollo server. The code for apolloclient configuration is given below.
// Apollo packages
import { ApolloClient } from "apollo-boost-upload";
import { WebSocketLink } from "apollo-link-ws";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import VueApollo from "vue-apollo";
Vue.use(VueApollo);
wsLink = new WebSocketLink({
uri: "ws://localhost:4000/graphql", // use wss for a secure endpoint
options: {
reconnect: true,
},
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
// Cache implementation
export const defaultClient = new ApolloClient({
// uri: "http://localhost:4000/graphql",
link,
cache: new InMemoryCache(),
fetchOptions: {
credentials: "include",
},
request: (operation) => {
// if no token in local storage, add it
if (!localStorage.anaceptToken) {
localStorage.setItem("anaceptToken", "");
}
// operation adds the token to authorizatrion header, which is sent o backend
operation.setContext({
headers: {
authorization: "Bearer " + localStorage.getItem("anaceptToken"),
},
});
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError);
}
if (graphQLErrors) {
for (const error of graphQLErrors) {
console.dir(error);
console.log(error);
if (
error.name === "AuthenticationError" ||
error.message === "jwt expired"
) {
// set auth error in state
store.commit("setError", error);
// signout user to clear error
store.dispatch("signUserOut");
}
}
}
},
});
vue config file
const { defineConfig } = require("#vue/cli-service");
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
module.exports = defineConfig({
pluginOptions: {
apollo: {
enableMocks: true,
enableEngine: true,
},
},
transpileDependencies: ["vuetify"],
chainWebpack: (config) => {
config.performance.maxEntrypointSize(400000).maxAssetSize(400000);
new NodePolyfillPlugin();
},
});
interesting try localhost 4004, it should work

Why 'new WebSocket()' doesn't work for nestjs?

I am sending with const socket = new WebSocket('ws://localhost:3000'); socket.send('hello world'); from client and i receive log 'connected' log on the server but not 'hello world'. socket.send() not working for NestJS. When I look at chrome network. It sends the data but not receiving in server.
here is the code: chat.gateway.ts
#WebSocketGateway()
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
handleConnection(client: any, ...args: any[]): any {
console.log('connected');
}
handleDisconnect(client: any): any {
console.log(client);
console.log('disconnected');
}
#SubscribeMessage('message')
handleEvent(client: any, data: any): WsResponse<any> {
const event = 'events';
return { event, data };
}
afterInit(server: any): any {
console.log(server.path);
}
}
main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { WsAdapter } from '#nestjs/platform-ws';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:4200',
credentials: true,
});
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(3000);
}
bootstrap();
Check out the Github examples: https://github.com/nestjs/nest/tree/master/sample/16-gateways-ws
On the client side:
const socket = new WebSocket('ws://localhost:80');
socket.onopen = () => {
console.log('Connected');
socket.send(
JSON.stringify({
event: 'message',
data: 'my very important message',
}),
);
socket.onmessage = (data) => {
console.log(data);
};
};
This should do the trick

using a plugin and store in middleware [Nuxt]

I want to write a middleware that checks the authentication and entitlement of the user. I get the authentication details from my store:
//store/index.js
const state = () => ({
auth: {
isLoggedIn: false
// and so on
}
});
and the entitlements from a plugin:
//plugins/entitlement.js
import axios from 'axios';
export default (context, inject) => {
const { env: { config: { entitlementUrl } }, store: { state: { auth: { access_token } } } } = context;
const headers = {
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json'
};
inject('entitlement', {
isEntitled: (resourceId) => new Promise((resolve, reject) => {
axios.get(`${entitlementUrl}/entitlements`, { headers, params: { resourceId } })
.then(({ data }) => {
resolve(data.Count > 0);
})
.catch((error) => {
reject(error);
});
})
};
This is the middleware that I wrote but it doesn't work:
//middleware/isEntitled.js
export default function ({ app, store }) {
if(store.state.auth.isLoggedIn){
let isEntitled = app.$entitlement.isEntitled('someId');
console.log('entitled? ', isEntitled)
}
}
And then I add it to my config:
//nuxt.config.js
router: {
middleware: 'isEntitled'
},
I get the error isEntitled of undefined. All I want to do is to check on every page of application to see if the user is entitled! How can I achieve that?
If you look at the situation from the plugin side, you can do this:
First create a plugin:
export default ({app}) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
if(app.store.state.auth.isLoggedIn){
let isEntitled = app.$entitlement.isEntitled('someId');
console.log('entitled? ', isEntitled)
}
return next();
})
}
then add the plugin to your nuxt.config.js file:
plugins: [
'~/plugins/your-plugin.js',
],

Valid JWT is invalid for nestJS guard

I'm passing a JWT from my client application (nextJS with nextAuth) via credentials header to my backend nestJS application (which is using graphQL). In my nestJS backend application I'm trying to implement an auth guard, so I extract the JWT with a custom function in my jwt.strategy.ts
But the JwtStrategy is not accepting my valid signed token. To prove that the JWT is valid, I put some console output for the token. But the validate() function is never called. I do not understand why, as the token can be validated with jwt.verify:
This is my output - it gets decoded by the jwt.verify():
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6MTIzLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaXNBZG1pbiI6dHJ1ZX0sImlhdCI6MTYwOTY3NTc4Nn0.LQy4QSesxJR91PyGGb_0mGZjpw9hlC4q7elIDs2CkLo
Secret: uGEFpuMDDdDQA3vCtZXPKgBYAriWWGrk
Decoded: {
user: { userId: 123, username: 'username', isAdmin: true },
iat: 1609675786
}
I don't see, what I am missing and I even do not see how to debug it as there is no output in my jwt.strategy.ts and the validate-function is not called at all.
jwt.strategy.ts
import jwt from 'jsonwebtoken'
// import { JwtService } from '#nestjs/jwt'
import { Strategy } from 'passport-jwt'
import { PassportStrategy } from '#nestjs/passport'
import { Injectable } from '#nestjs/common'
import cookie from 'cookie'
import { getConfig } from '#myapp/config'
const { secret } = getConfig()
const parseCookie = (cookies) => cookie.parse(cookies || '')
const cookieExtractor = async (req) => {
let token = null
if (req?.headers?.cookie) {
token = parseCookie(req.headers.cookie)['next-auth.session-token']
}
// output as shown above
console.log('JWT:', token)
console.log('Secret:', secret)
const decoded = await jwt.verify(token, secret, { algorithms: ['HS256'] })
console.log('Decoded: ', decoded)
return token
}
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: true,
secretOrKey: secret
})
}
async validate(payload: any) {
console.log('payload:', payload) // is never called
return { userId: payload.sub, username: payload.username }
}
}
jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '#nestjs/common'
import { AuthGuard } from '#nestjs/passport'
import { GqlExecutionContext } from '#nestjs/graphql'
#Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: GqlExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
The guard is used in this resolver:
editor.resolver.ts
import { Query, Resolver } from '#nestjs/graphql'
import { UseGuards } from '#nestjs/common'
import { GqlAuthGuard } from '../auth/jwt-auth.guard'
#Resolver('Editor')
export class EditorResolvers {
constructor(private readonly editorService: EditorService) {}
#UseGuards(GqlAuthGuard)
#Query(() => [File])
async getFiles() {
return this.editorService.getFiles()
}
}
auth.module.ts
import { Module } from '#nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { PassportModule } from '#nestjs/passport'
import { LocalStrategy } from './local.strategy'
import { JwtStrategy } from './jwt.strategy'
import { UsersModule } from '../users/users.module'
import { JwtModule } from '#nestjs/jwt'
import { getConfig } from '#myApp/config'
const { secret } = getConfig()
#Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret,
verifyOptions: { algorithms: ['HS256'] },
signOptions: { expiresIn: '1d' }
})
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService]
})
export class AuthModule {}
The token is created on server side (nextJS api page) with:
const encode = async ({ secret, token }) => jwt.sign(token, secret, { algorithm: 'HS256' })
I see 2 differences from nestJS docs examples from your jwt.strategy.ts file that you can change and give it a try..
https://docs.nestjs.com/security/authentication#implementing-passport-jwt
sync extractor and not async
By default passport-jwt extractor we can see that is an sync and not async, so you can try change your extractor and remove the async, or to add await when calling it.
https://github.com/mikenicholson/passport-jwt/blob/master/lib/extract_jwt.js ,
look for fromAuthHeaderAsBearerToken function.
so or change your
const cookieExtractor = async (req) => {
to
const cookieExtractor = (req) => {
OR - add await when you call it
jwtFromRequest: await cookieExtractor(),
call the extractor and not just pass it
by the docs example in JwtStrategy constructor they calling the extractor and not passing it like you do
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
so try to call it in your JwtStrategy constructor
jwtFromRequest: cookieExtractor(), // (again - take care of sync / async)

How to use GraphQL subscription correctly?

I have a GraphQL powered app. The query and mutation parts work well. I try to add GraphQL subscription.
The server GraphQL subscription part code is inspired by the demo in the readme of apollographql/subscriptions-transport-ws.
Please also check the comments in the code for more details.
import Koa from 'koa';
import Router from 'koa-router';
import graphqlHTTP from 'koa-graphql';
import asyncify from 'callback-to-async-iterator';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import firebase from 'firebase-admin';
import { execute, subscribe } from 'graphql';
import { GraphQLObjectType, GraphQLString } from 'graphql';
const MeType = new GraphQLObjectType({
name: 'Me',
fields: () => ({
name: { type: GraphQLString },
// ...
}),
});
const listenMe = async (callback) => {
// Below the firebase API returns real-time data
return firebase
.database()
.ref('/users/123')
.on('value', (snapshot) => {
// snapshot.val() returns an Object including name field.
// Here I tested is correct, it always returns { name: 'Rose', ... }
// when some other fields inside got updated in database.
return callback(snapshot.val());
});
};
const Subscription = new GraphQLObjectType({
name: 'Subscription',
fields: () => ({
meChanged: {
type: MeType,
subscribe: () => asyncify(listenMe),
},
}),
});
const schema = new GraphQLSchema({
query: Query,
mutation: Mutation,
subscription: Subscription,
});
const app = new Koa();
app
.use(new Router()
.post('/graphql', async (ctx) => {
// ...
await graphqlHTTP({
schema,
graphiql: true,
})(ctx);
})
.routes());
const server = app.listen(3009);
SubscriptionServer.create(
{
schema,
execute,
subscribe,
},
{
server,
path: '/subscriptions',
},
);
I am using Altair GraphQL Client to test since it supports GraphQL subscription.
As the screenshot shows, it does get new data every time when the data changes in database.
However, meChanged is null and it does not throw any error. Any idea? Thanks
Finally have a new library can do the work without full Apollo framework.
https://github.com/enisdenjo/graphql-ws
Here are the codes that I have succeed:
Server (GraphQL Schema Definition Language)
import { useServer } from 'graphql-ws/lib/use/ws';
import WebSocket from 'ws';
import { buildSchema } from 'graphql';
const schema = buildSchema(`
type Subscription {
greeting: String
}
`);
const roots = {
subscription: {
greeting: async function* sayHiIn5Languages() {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greeting: hi };
}
},
},
};
const wsServer = new ws.Server({
server, // Your HTTP server
path: '/graphql',
});
useServer(
{
schema,
execute,
subscribe,
roots,
},
wsServer
);
Server (GraphQL.js GraphQLSchema object way)
import { execute, subscribe, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
import { useServer } from 'graphql-ws/lib/use/ws';
import WebSocket from 'ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
greeting: {
type: GraphQLString,
resolve: (source) => {
if (source instanceof Error) {
throw source;
}
return source.greeting;
},
subscribe: () => {
return pubsub.asyncIterator('greeting');
},
},
},
});
const schema = new GraphQLSchema({
query,
mutation,
subscription,
});
setInterval(() => {
pubsub.publish('greeting', {
greeting: 'Bonjour',
});
}, 1000);
const wsServer = new ws.Server({
server, // Your HTTP server
path: '/graphql',
});
useServer(
{
schema,
execute,
subscribe,
roots,
},
wsServer
);
Client
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://localhost:5000/graphql',
});
client.subscribe(
{
query: 'subscription { greeting }',
},
{
next: (data) => {
console.log('data', data);
},
error: (error) => {
console.error('error', error);
},
complete: () => {
console.log('no more greetings');
},
}
);
DISCLOSE: I am not associated with the library.

Categories

Resources