My web app is using:
NextJS
NextAuth.js
Apollo Server
I have a NextAuth set up in my app, and I am able to log in just fine.
The problem is coming from trying to get access to the user's session in the Apollo context. I want to pass my user's session into every resolver. Here's my current code:
import { ApolloServer, AuthenticationError } from "apollo-server-micro";
import schema from "./schema";
import mongoose from "mongoose";
import dataloaders from "./dataloaders";
import { getSession } from "next-auth/client";
let db;
const apolloServer = new ApolloServer({
schema,
context: async ({ req }) => {
/*
...
database connection setup
...
*/
// get user's session
const userSession = await getSession({ req });
console.log("USER SESSION", userSession); // <-- userSession is ALWAYS null
if (!userSession) {
throw new AuthenticationError("User is not logged in.");
}
return { db, dataloaders, userSession };
},
});
export const config = {
api: {
bodyParser: false,
},
};
export default apolloServer.createHandler({ path: "/api/graphql" });
The problem is, the session (userSession) is always null, even if I am logged in (and can get a session just fine from a proper NextJS API route). My guess is that because the NextAuth function used to get the session, getSession({ req }) is being passed req--which is provided from Apollo Server Micro, and not from NextJS (which NextAuth is expecting). I've done a lot of searching and can't find anyone who's had this same problem. Any help is much appreciated!
I had exactly this issue and I found it was because of the Apollo GraphQL playground.
The playground does not send credentials without "request.credentials": "include".
My NextAuth / GraphQL API looks like this:
import { ApolloServer } from "apollo-server-micro";
import { getSession } from "next-auth/client";
import { typeDefs, resolvers } "./defined-elsewhere"
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const session = await getSession({ req });
return { session };
},
playground: {
settings: {
"editor.theme": "light",
"request.credentials": "include",
},
},
});
Hope this works for you!
I just ran into something similar. I'm not 100% sure because it's hard to know the exact details since your example code above doesn't show how you're interacting with apollo from the client before the session is coming through as null. I believe however that you're probably making an API call from inside the getStaticProps which causes static code generation and gets run at build time - ie when no such user context / session could possibly exist.
See https://github.com/nextauthjs/next-auth/issues/383
The getStaticProps method in Next.js is only for build time page generation (e.g. for generating static pages from a headless CMS) and cannot be used for user specific data such as sessions or CSRF Tokens.
Also fwiw I'm not sure why you got downvoted - seems like a legit question to ask imo even if the answer is mostly a standard rtm :). Has happened to me here before too - you win some you lose some :) Cheers
Related
I'm creating a MERN-G food ordering app. When I query my database in the Apollo sandbox, I get all the data I've seeded my database with as expected, but when I do it in my code, I can't get past loading. Here is my code
import { useQuery } from "#apollo/client";
import { QUERY_MENU } from "../utils/queries";
export default function Menu() {
const {data, loading} = useQuery(QUERY_MENU)
useEffect(() => {
if(loading){
console.log("Loading...");
} else {
console.log(data);
}
}, [data, loading])
It never seems to change from loading.
I tried logging the data, and expected to get back the same result I'm seeing in my apollo sandbox. I am using the exact query used in the sandbox in my code.
Here are my typedefs and resolvers from my server side.
typedefs
resolvers
And my utils/queries file for the client side
utils/queries
I have a page where users can give themselves a "role" like member or admin. They can go to another route to create messages. I am trying to update user's role from "user" to "admin". It updates req.session to admin role in the admin.js file, but when I go to messages/create.js and try to log req.session, it shows that user still has the "user" role. I am saving the changes I make by calling req.session.save(), but it is not working.
admin.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../lib/config";
import Users from "../../models/user";
import { connectToDatabase } from "../../util/mongodb";
const handler = nc()
handler.post(async (req) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
}
})
export default withIronSessionApiRoute(handler, session_config);
messages/create.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../../lib/config";
const handler = nc()
handler.post(async (req) => {
console.log(req.session.user)
console.log(req.body)
})
export default withIronSessionApiRoute(handler, session_config)
Please let me know what the issue is and how I can fix it. Thank you
The first thing I noticed from looking at the code is that you're not sending a response back to the client. Iron session uses cookies to manage stateless authentication and the way it manages is by setting the response header. Because you're not sending a response, it can't update the session.
Looking further into the API documentation, session.save() - "Saves the session and sets the cookie header to be sent once the response is sent."
Not knowing your full implementation or having a working code example from something like codesandbox.io, I suggest the following code to see if this solves your problem.
// please make sure that `res` is a parameter on the `.post()` function
// on your original code. I've already set it as shown below.
handler.post(async (req, res) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
// response below
res.send({ ok: true })
// or if you don't want to send custom data back, comment the line above,
// and then uncomment the line below
// res.status(200).end()
}
})
Attempt 2
I made an iron session demo on Codesandbox using some of the demo code from the iron session repo NextJs example.
The code example shows:
login
log out
setting a user as an admin
fetching user data from server-side
fetching user data from client-side
fetching using SWR
Some side notes to be aware of: if you are doing something like
const sessionData = req.session.user, then trying to mutate the req.session.user, and then sending the data back, it won't work because the session object will be recreated per request and node cannot store req.session as a reference.
If my demo doesn't help you, then you're going to have to share more info and code, and maybe create a Codesandbox to reproduce what is happening to you.
Good morning,
I have an Angular WebApp that uses GraphQL codegen with (apollo-angular plugin and all the typescript plugins). Everything works fine but I want to handle Hasura Roles and Hasura User ID. From Hasura Console everything is configured correctly and working.
Only thing I am missing is how to handle this on the front end. I need to add X-Hasura-Role and X-Hasura-User-Id headers to every request sent to Hasura.
Is there a way to do this with graphql-codegen?
What is the right way to do this?
I know I can add the headers section on the codegen.yml, but obviously the role and userid are dynamic so I cannot hardcode anything there.
Should I use maybe a customFetch component? This component, thought, should only intercept every request sent to Hasura and add the headers needed. I have no idea how to do this so I hope you can help me (I also hope there is a better solution)
Best regards
When you create your Apollo client instance in the Angular application you can set it up to pass along the Authorization header which should contain the user's id and their roles.
There are examples of this in the Angular Apollo docs. Eg:
import { NgModule } from '#angular/core';
import { HttpClientModule } from '#angular/common/http';
import { Apollo, APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache,ApolloLink } from '#apollo/client/core';
import { setContext } from '#apollo/client/link/context';
const uri = '/graphql';
export function createApollo(httpLink: HttpLink) {
const basic = setContext((operation, context) => ({
headers: {
Accept: 'charset=utf-8'
}
}));
const auth = setContext((operation, context) => {
const token = localStorage.getItem('token');
if (token === null) {
return {};
} else {
return {
headers: {
Authorization: `JWT ${token}`
}
};
}
});
const link = ApolloLink.from([basic, auth, httpLink.create({ uri })]);
const cache = new InMemoryCache();
return {
link,
cache
}
}
#NgModule({
exports: [
HttpClientModule,
],
providers: [{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink]
}]
})
export class GraphQLModule {}
It is up to you to ensure that the JWT token that will be passed along with your request is available in the front end. Ultimately you're going to have to implement some kind of authentication approach to allow the user to sign in and pass the token to your front end application.
More information is available in the Hasura Docs for Authentication
There are also a number of tutorials and guides for integrating with different third party auth providers
I'm starting to work with GraphQL and the new Nexus Framework GraphQL server, which is a great product.
On my server-side, I defined my schema, I can query my database with Prisma and everything runs smoothly. I can query data also from the Nexus GraphQL playground and also with Postman.
Now, I want to make things work on the client-side. I see that Apollo Client is the best solution to integrate React with GraphQL, but I just can't make things work. I read tons of docs but I'm missing something that I can't figure out.
GraphQL and the client part will be hosted on the same server, on separate node applications.
I'm configuring Apollo based on its documentations. The example below is for the new 3.0 Beta Version of Apollo which I'm testing, but the same scenario happens on the last stable version. I believe that I need to do something else to integrate Apollo and Nexus.
Every query returns: "Must Provide Query String".
The same query inside the playground works perfectly.
Here is my basic testing code:
apollo.js:
import { ApolloClient, HttpLink, InMemoryCache } from '#apollo/client'
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
fetchOptions: {
mode: 'no-cors',
}
})
})
export default client
App.js:
import React from 'react'
import { ApolloProvider } from '#apollo/client';
import client from './database/apollo'
import Home from './components/Home'
const App = () => {
return (
<ApolloProvider client={client}>
<Home />
</ApolloProvider>
)
}
export default App;
Home.js:
import React, { useState, useEffect, useReducer } from 'react'
import { useQuery, gql } from '#apollo/client'
const PUBLICATIONS = gql`
{
albumreviews(last: 1) {
title
}
}
`
const Home = () =>{
const { loading, error, data } = useQuery(PUBLICATIONS)
if (loading) return <p>Loading...</p>
if (error) return <p>Error :(</p>
return data.albumreviews.map(({ review }) => (
<div>{JSON.parse(review)}</div>
))
}
export default Home
On the client-side: "Error" is displayed.
On the server-side: "Must provide query string"
Believe me, I've tried to adjust the query thousands of times trying to get a different answer.
Could some help me to move forward with this? Should I provide the Nexus schema to the apollo client? What is the better way of doing this?
You should pretty much never use no-cors. Off hand, I'm not sure why that option would cause your request to be malformed, but it will make it impossible for your response to be read anyway. Remove fetchOptions and whitelist your client URL in your CORS configuration on the server-side. CORs usage with Nexus is shown here in the docs.
When using apollo-server 2.2.1 or later, how can one log, for each request, the query and the variables?
This seems like a simple requirement and common use case, but the documentation is very vague, and the query object passed to formatResponse no longer has the queryString and variables properties.
Amit's answer works (today), but IMHO it is a bit hacky and it may not work as expected in the future, or it may not work correctly in some scenarios.
For instance, the first thing that I thought when I saw it was: "that may not work if the query is invalid", it turns out that today it does work when the query is invalid. Because with the current implementation the context is evaluated before the the query is validated. However, that's an implementation detail that can change in the future. For instance, what if one day the apollo team decides that it would be a performance win to evaluate the context only after the query has been parsed and validated? That's actually what I was expecting :-)
What I'm trying to say is that if you just want to log something quick in order to debug something in your dev environment, then Amit's solution is definitely the way to go.
However, if what you want is to register logs for a production environment, then using the context function is probably not the best idea. In that case, I would install the graphql-extensions and I would use them for logging, something like:
const { print } = require('graphql');
class BasicLogging {
requestDidStart({queryString, parsedQuery, variables}) {
const query = queryString || print(parsedQuery);
console.log(query);
console.log(variables);
}
willSendResponse({graphqlResponse}) {
console.log(JSON.stringify(graphqlResponse, null, 2));
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
extensions: [() => new BasicLogging()]
});
Edit:
As Dan pointed out, there is no need to install the graphql-extensions package because it has been integrated inside the apollo-server-core package.
With the new plugins API, you can use a very similar approach to Josep's answer, except that you structure the code a bit differently.
const BASIC_LOGGING = {
requestDidStart(requestContext) {
console.log("request started");
console.log(requestContext.request.query);
console.log(requestContext.request.variables);
return {
didEncounterErrors(requestContext) {
console.log("an error happened in response to query " + requestContext.request.query);
console.log(requestContext.errors);
}
};
},
willSendResponse(requestContext) {
console.log("response sent", requestContext.response);
}
};
const server = new ApolloServer(
{
schema,
plugins: [BASIC_LOGGING]
}
)
server.listen(3003, '0.0.0.0').then(({ url }) => {
console.log(`GraphQL API ready at ${url}`);
});
If I had to log the query and variables, I would probably use apollo-server-express, instead of apollo-server, so that I could add a separate express middleware before the graphql one that logged that for me:
const express = require('express')
const { ApolloServer } = require('apollo-server-express')
const { typeDefs, resolvers } = require('./graphql')
const server = new ApolloServer({ typeDefs, resolvers })
const app = express()
app.use(bodyParser.json())
app.use('/graphql', (req, res, next) => {
console.log(req.body.query)
console.log(req.body.variables)
return next()
})
server.applyMiddleware({ app })
app.listen({ port: 4000}, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
})
Dan's solution mostly resolves the problem but if you want to log it without using express,
you can capture it in context shown in below sample.
const server = new ApolloServer({
schema,
context: params => () => {
console.log(params.req.body.query);
console.log(params.req.body.variables);
}
});
I found myself needing something like this but in a more compact form - just the query or mutation name and the ID of the user making the request. This is for logging queries in production to trace what the user was doing.
I call logGraphQlQueries(req) at the end of my context.js code:
export const logGraphQlQueries = ( req ) => {
// the operation name is the first token in the first line
const operationName = req.body.query.split(' ')[0];
// the query name is first token in the 2nd line
const queryName = req.body.query
.split('\n')[1]
.trim()
.split(' ')[0]
.split('(')[0];
// in my case the user object is attached to the request (after decoding the jwt)
const userString = req.user?.id
? `for user ${req.user.id}`
: '(unauthenticated)';
console.log(`${operationName} ${queryName} ${userString}`);
};
This outputs lines such as:
query foo for user e0ab63d9-2513-4140-aad9-d9f2f43f7744
Apollo Server exposes a request lifecycle event called didResolveOperation at which point the requestContext has populated properties called operation and operationName
plugins: [
{
requestDidStart(requestContext) {
return {
didResolveOperation({ operation, operationName }) {
const operationType = operation.operation;
console.log(`${operationType} recieved: ${operationName}`)
}
};
}
}
]
// query recieved: ExampleQuery
// mutation recieved: ExampleMutation