I'm relatively new to next-auth & next in general. We have an application which we are transitioning to using AWS Cognito for authentication. It's a server side rendered application & I know this as under pages/_app.tsx is getInitialProps(). The code being:
import React from "react";
import App from "next/app";
import Router from "next/router";
import { Provider } from "react-redux";
import withRedux from "next-redux-wrapper";
import ReduxToastr from "react-redux-toastr";
import NProgress from "nprogress";
import * as Sentry from "#sentry/node";
// import { setTokens } from "../util/tokens";
import jwtDecode from "jwt-decode";
import { SessionProvider, getSession } from "next-auth/react";
import createStore from "../redux/store";
import { setSession } from "../redux/session";
import "semantic-ui-css/semantic.min.css";
import "react-redux-toastr/lib/css/react-redux-toastr.min.css";
import "../styles/nprogress.css";
import "../styles/overrides.scss";
import "../styles/util.css";
import axios from "axios";
import { IdToken, CognitoTokens } from "../interfaces";
Router.events.on("routeChangeStart", () => {
NProgress.start();
});
Router.events.on("routeChangeComplete", () => NProgress.done());
Router.events.on("routeChangeError", () => NProgress.done());
NProgress.configure({ showSpinner: false });
type tProps = {
passport?: {};
};
class MyApp extends App {
static async getInitialProps({ Component, ctx }: any) {
debugger;
let pageProps: tProps = {};
const session = await getSession({
req: ctx.req,
}); // this calls '/api/auth/session'
// let tokens: AuthTokens = {};
if (ctx.isServer && ctx.query?.code) {
const {
AUTH_DOMAIN,
COGNITO_CLIENT_ID,
COGNITO_CLIENT_SECRET,
BASE_URL,
} = process.env;
const { data }: { data: CognitoTokens } = await axios.post(
`https://${AUTH_DOMAIN}/oauth2/token?client_id=${COGNITO_CLIENT_ID}&client_secret=${COGNITO_CLIENT_SECRET}&grant_type=authorization_code&redirect_uri=${BASE_URL}&code=${ctx.query.code}`,
);
}
return { pageProps };
}
render() {
debugger
const { Component, pageProps, store }: any = this.props;
return (
<SessionProvider session={pageProps.session}>
<Provider store={store}>
<Component {...pageProps} />
<ReduxToastr
preventDuplicates
position="bottom-right"
transitionIn="fadeIn"
transitionOut="fadeOut"
timeOut={5000}
progressBar
/>
</Provider>
</SessionProvider>
);
}
}
export default withRedux(createStore)(MyApp);
pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import CognitoProvider from "next-auth/providers/cognito";
export default NextAuth({
providers: [
CognitoProvider({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
issuer: process.env.COGNITO_ISSUER,
}),
],
debug: process.env.NODE_ENV === "development" ? true : false,
});
There is authentication middleware defined for the API. The main reason for this is that we have a custom permissions attribute defined on a user in Cognito which defines what CRUD operations they can do in specific environments
server/index.ts
import express from "express";
import next from "next";
import bodyParser from "body-parser";
import session from "express-session";
import connectMongodbSession from "connect-mongodb-session";
// Config
import { /* auth0Strategy, */ getSessionConfig } from "./config";
const port = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
// API
import authAPI from "./api/auth";
import accountsAPI from "./api/accounts";
import usersAPI from "./api/users";
import jobsAPI from "./api/jobs";
import awsAPI from "./api/aws";
import completedJobsAPI from "./api/completedJobs";
// middleware
import { restrictAccess, checkPermissions } from "./middleware/auth";
// Server
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
const MongoDBStore = connectMongodbSession(session);
const store = new MongoDBStore({
uri: process.env.MONGO_PROD_URL,
collection: "godfatherSessions",
});
server.use(bodyParser.json());
server.use(session(getSessionConfig(store)));
server.get("/healthcheck", async (req: any, res, next) => {
const { db } = req.session.req.sessionStore;
try {
await db.command({
ping: 1,
});
res.status(200).end();
} catch (err) {
next(err);
}
});
// Middleware
server.use(checkPermissions);
// API
server.use(authAPI);
server.use(accountsAPI, restrictAccess);
server.use(usersAPI, restrictAccess);
server.use(jobsAPI, restrictAccess);
server.use(awsAPI, restrictAccess);
server.use(completedJobsAPI, restrictAccess);
// Protected Page Routes
server.get("/accounts", restrictAccess, (req: any, res) =>
app.render(req, res, "/accounts", req.query),
);
server.get("/users", restrictAccess, (req: any, res) =>
app.render(req, res, "/users", req.query),
);
server.get("/jobs", restrictAccess, (req: any, res) =>
app.render(req, res, "/jobs", req.query),
);
server.get("/completedJobs", restrictAccess, (req: any, res) => {
app.render(req, res, "/completedJobs", req.query);
});
server.get("/debug-sentry", function mainHandler() {
throw new Error("My first Sentry error!");
});
// Fallback Routes
server.all("*", handle as any);
// The error handler must be before any other error middleware and after all controllers
// server.use(Sentry.Handlers.errorHandler());
server.listen(port, (err?: Error) => {
if (err) throw err;
console.log(
`> Server listening at http://localhost:${port} as ${
dev ? "development" : process.env.NODE_ENV
}`,
);
});
});
server/middleware/auth.ts
/* eslint-disable #typescript-eslint/camelcase */
import "cross-fetch/polyfill";
import { Request, Response, NextFunction } from "express";
import { ForbiddenError } from "../../util/errors";
import { Environments } from "../../interfaces";
import jwt_decode from "jwt-decode";
import { CognitoUser, CognitoUserPool } from "amazon-cognito-identity-js";
import { getSession } from "next-auth/react";
export async function checkPermissions(
req: CustomRequest,
res: Response,
next: NextFunction,
) {
const { path, method } = req;
const { authorization, env } = req.headers;
const session = await getSession({ req });
debugger;
if (
!req.url.includes("/api/") ||
["/api/auth/callback/cognito", "/api/auth/session"].includes(req.path)
) {
return next();
}
if (req.headers.env === "development") {
return next();
}
console.log(session);
if (!authorization) {
return res.status(401).redirect("/login");
}
const decoded: any = jwt_decode(authorization);
const invalidAuth = isInvalidAuth(decoded, path);
if (!invalidAuth) {
const userPool = new CognitoUserPool({
UserPoolId: process.env.COGNITO_USER_POOL_ID,
ClientId: process.env.COGNITO_CLIENT_ID,
});
const username = decoded.username || decoded["cognito:username"];
const cognitoUser = new CognitoUser({
Username: username,
Pool: userPool,
});
const userAttributes: any = await getUserAttributes(cognitoUser);
const permissions = userAttributes.permissions
.split(",")
.map(perm => `"${perm}"`);
const hasPermission = getHasPermission(
method,
permissions,
env as Environments,
);
if (!hasPermission) {
return res
.status(403)
.send(
new ForbiddenError(
"You do not have permission to perform this action.",
).toErrorObject(),
);
}
return next();
}
return next(invalidAuth);
}
The callback URL defined in Cognito app client is "http://localhost:3000/api/auth/callback/cognito" (for development purposes - as is highlighted in the example).
When running the application & logging in via the Cognito hosted UI, the 1st breakpoint hit is the debugger on L5 in checkPermissions. The session is null at this point & the path is /api/auth/session.
The issue comes when I step through past that breakpoint to return next() where I get the error(s) shown in the picture below. So these are ECONNRESET errors & if I let it run freely, these errors occur over & over again resulting in an EMFILE error every now & again until eventually the application crashes. No other breakpoints are hit.
Versions:
next: v9.3.5
next-auth: v4.1.0
Even though, by default, next-auth will use localhost:3000, I kept getting an ENOTFOUND error when I didn't define the URL in the .env so I set it to:
NEXTAUTH_URL=http://127.0.0.1:3000
After reading all docs & examples & several tutorials on how to do it (no one has done a tutorial on v4 - just v3) I'm still stumped
For me helped to remove .next/ and add these lines to .env
NODE_TLS_REJECT_UNAUTHORIZED="0"
NEXTAUTH_URL="http://127.0.0.1:3000"
NEXTAUTH_URL_INTERNAL="http://127.0.0.1:3000"
It seems to work very fine.
Related
My main.ts looks like this :
import { NestFactory } from '#nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '#nestjs/platform-fastify';
import { Logger } from 'nestjs-pino';
import { processRequest } from 'graphql-upload';
import { AppModule } from './app.module';
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastifyInstance = adapter.getInstance();
fastifyInstance.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastifyInstance.addHook('preValidation', async (request: any, reply) => {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
{ bufferLogs: true },
);
app.useLogger(app.get(Logger));
app.enableCors();
await app.listen(parseInt(process.env.SERVER_PORT || '3000', 10), '0.0.0.0');
}
bootstrap();
According to the fastify doc the body limit is 1MiB by default, however I want it to be larger. So I tried like this :
const adapter = new FastifyAdapter({ bodyLimit: 124857600 }); but I still get the same problem with my payload being too large.
Try to add this when you are creating the app
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ bodyLimit: 10048576 }),
Server: Node.js, express, Type-Graphql with Apollo Server
In index.ts:
import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { createConnection } from 'typeorm';
import { verify } from 'jsonwebtoken';
import coockieParser from 'cookie-parser';
import cors from 'cors';
import User from './entity/User';
import UserResolver from './resolvers';
import { createAccessToken, createRefreshToken, sendRefreshToken } from './auth';
require('dotenv').config();
const corsOptions = {
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Access-Token', 'Authorization'],
credentials: true, // this allows to send back (to client) cookies
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
origin: 'http://localhost:3000',
preflightContinue: false,
};
(async () => {
const PORT = process.env.PORT || 4000;
const app = express();
app.use(coockieParser());
app.use(cors(corsOptions));
// -- non graphql endpoints
app.get('/', (_, res) => {
res.send('Starter endpoint');
});
app.post('/refresh_token', async (req, res) => {
const token = req.cookies.jid;
if (!token) {
return res.send({ ok: false, accessToken: '' });
}
let payload: any = null;
try {
payload = verify(token, process.env.REFRESH_TOKEN_SECRET!);
} catch (e) {
console.log(e);
return res.send({ ok: false, accessToken: '' });
}
// token is valid, and the access token can be send back
const user = await User.findOne({ id: payload.userId });
if (!user) {
return res.send({ ok: false, accessToken: '' });
}
if (user.tokenVersion !== payload.tokenVersion) {
return res.send({ ok: false, accessToken: '' });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ ok: true, accessToken: createAccessToken(user) });
});
//--
// -- db
await createConnection();
// --
// -- apollo server settings
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [UserResolver],
}),
context: ({ req, res }) => ({ req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: false,
});
// --
app.listen(PORT, () => {
console.log(`Server running on port: ${PORT}`);
});
})();
Login mutation in the UserResolver:
//..
#Mutation(() => LoginResponse)
async login(
#Arg('email') email: string,
#Arg('password') password: string,
#Ctx() { res }: AuthContext,
): Promise<LoginResponse> {
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('Incorrect email');
}
const valid = await compare(password, user.password);
if (!valid) {
throw new Error('Incorrect password');
}
sendRefreshToken(res, createRefreshToken(user));
return {
accessToken: createAccessToken(user),
user,
};
}
//..
When handling authentification, the cookies are set in the response header as follows:
//..
export const createAccessToken = (user: User) => sign({ userId: user.id }, process.env.ACCESS_TOKEN_SECRET!, { expiresIn: '10m' });
export const createRefreshToken = (user: User) => sign({ userId: user.id, tokenVersion: user.tokenVersion }, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: '7d' });
export const sendRefreshToken = (res: Response, refreshToken: string) => {
res.cookie('jid', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/refresh_token',
});
};
//..
Client: Next.js, Graphql with URQL
In _app.tsx:
/* eslint-disable react/jsx-props-no-spreading */
import * as React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';
import { ThemeProvider } from '#mui/material/styles';
import CssBaseline from '#mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '#emotion/react';
import { createClient, Provider } from 'urql';
import theme from '../styles/theme';
import createEmotionCache from '../lib/createEmotionCache';
import '../styles/globals.css';
// Client-side cache shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface IAppProps extends AppProps {
// eslint-disable-next-line react/require-default-props
emotionCache?: EmotionCache;
}
const client = createClient({
url: 'http://localhost:4000/graphql',
fetchOptions: {
credentials: 'include',
},
});
const App = (props: IAppProps) => {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
return (
<Provider value={client}>
<CacheProvider value={emotionCache}>
<Head>
<title>Client App</title>
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</Provider>
);
};
export default App;
Login page does not rely on SSR or SSG (so it is CSR):
import React from 'react';
import LoginForm from '../components/LoginForm/LoginForm';
import Layout from '../layouts/Layout';
interface ILoginProps {}
const Login: React.FC<ILoginProps> = () => (
<Layout
showNavbar={false}
showTransition={false}
maxWidth='xs'
>
<LoginForm />
</Layout>
);
export default Login;
The mutation is used in the LoginForm component to request an access token and set refresh token in the browser cookies:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
Issue
So, the problem is that the login response has set-cookie in the header, but the cookie still isn't set in the browser:
Question
Previously, I've implemented the same authentication scheme using the same server code but create-react-app on the client. Everything worked just fine. So, why isn't it working now with next.js? What am I missing?
Work Around
I can use something like cookies-next to put cookies into the storage. The refresh token then would need to be passed in the response data:
import React from 'react';
import { useRouter } from 'next/router';
import { useLoginMutation } from '../../generated/graphql';
import { setCookies } from 'cookies-next';
//...
const LoginForm = () => {
//..
const [, login] = useLoginMutation();
const router = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (disabledSubmit) {
setShowFormHelper(true);
} else {
const res = await login({
email, // from the state of the component
password,
});
if (res && res.data?.login) {
console.log(res.data.login.accessToken);
setCookies('jid', res.data.login.refreshToken);
router.push('/home');
setShowFormHelper(false);
} else {
setHelper('Something went wrong');
}
}
};
//..
};
export default LoginForm;
setCookie accepts options. However, the httpOnly can't be set to true in this case anyway.
Updates
It turns out everything above works in Firefox, but not in Chrome.
in res.cookie defined in the express server, use sameSite:'lax' instead of strict. this may solve the issue.
Greetings I have a problem every time when I want to make an Admin REST API call to Shopify I get this problem "Error: Failed to parse session token '****' jwt expired" I see some code examples on the net I have my own custom session storage for accessToken and shop but every time when I try to call my own route from front-end and get more details about the shop I get this problem here is code example can anyone help me?
server.js
import "#babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "#shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "#shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
const helmet = require("koa-helmet");
const compress = require("koa-compress");
const cors = require("koa-cors");
const logger = require("koa-logger");
const bodyParser = require("koa-bodyparser");
import axios from "axios";
import { storeCallback, loadCallback, deleteCallback } from "./custom-session";
const sequelize = require("./database/database");
const { Shopify_custom_session_storage } = require("./../models/sequelizeModels");
// import apiRouter from "./../routers/apiRouter";
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SCOPES.split(","),
HOST_NAME: process.env.HOST.replace(/https:\/\/|\/$/g, ""),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: true,
// This should be replaced with your preferred storage strategy
SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(storeCallback, loadCallback, deleteCallback)
});
sequelize.sync()
.then(() => {
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
// Getting users data from database and saving it to variable //
try {
await Shopify_custom_session_storage.findAll({
raw: true,
where:{
shop: shop
},
limit:1
});
} catch(err) {
console.log(err);
throw err;
}
// End of Getting users data from database and saving it to variable //
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>{
return Shopify_custom_session_storage.destroy({
where: {
shop: shop
}
})
.then(result => {
return true;
})
.catch(err => {
if(err) throw err;
return false;
});
}
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`);
},
})
);
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.post("/webhooks", async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
console.log(`Webhook processed, returned status code 200`);
} catch (error) {
console.log(`Failed to process webhook: ${error}`);
}
});
router.post("/graphql", verifyRequest({ returnHeader: true }), async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
// Our Routes //
router.get("/getProducts", verifyRequest({ returnHeader: true }), async (ctx) => {
try{
const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
const client = new Shopify.Clients.Rest(session.shop, session.accessToken);
console.log(session);
}catch(err) {
console.log(err);
throw new Error(err);
}
});
// End of Our Routes //
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
try {
let user = await Shopify_custom_session_storage.findAll({
raw: true,
where:{
shop: shop
},
limit:1
});
// This shop hasn't been seen yet, go through OAuth to create a session
if (user[0].shop == undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
} catch(err) {
console.log(err);
throw err;
}
});
server.use(router.allowedMethods());
server.use(router.routes());
// Setting our installed dependecies //
server.use(bodyParser());
server.use(helmet());
server.use(cors());
server.use(compress());
server.use(logger());
// End of Setting our installed dependecies //
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
})
.catch((err) => {
if(err) throw err;
return process.exit(1);
})
_app.js
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "#shopify/polaris";
import { Provider, useAppBridge } from "#shopify/app-bridge-react";
import { authenticatedFetch, getSessionToken } from "#shopify/app-bridge-utils";
import { Redirect } from "#shopify/app-bridge/actions";
import "#shopify/polaris/dist/styles.css";
import translations from "#shopify/polaris/locales/en.json";
import axios from 'axios';
function userLoggedInFetch(app) {
const fetchFunction = authenticatedFetch(app);
return async (uri, options) => {
const response = await fetchFunction(uri, options);
if (
response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
) {
const authUrlHeader = response.headers.get(
"X-Shopify-API-Request-Failure-Reauthorize-Url"
);
const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
return null;
}
return response;
};
}
function MyProvider(props) {
const app = useAppBridge();
const client = new ApolloClient({
fetch: userLoggedInFetch(app),
fetchOptions: {
credentials: "include",
},
});
const axios_instance = axios.create();
// Intercept all requests on this Axios instance
axios_instance.interceptors.request.use(function (config) {
return getSessionToken(app) // requires a Shopify App Bridge instance
.then((token) => {
// Append your request headers with an authenticated token
config.headers["Authorization"] = `Bearer ${token}`;
return config;
});
});
const Component = props.Component;
return (
<ApolloProvider client={client}>
<Component {...props} axios_instance={axios_instance}/>
</ApolloProvider>
);
}
class MyApp extends App {
render() {
const { Component, pageProps, host } = this.props;
return (
<AppProvider i18n={translations}>
<Provider
config={{
apiKey: API_KEY,
host: host,
forceRedirect: true,
}}
>
<MyProvider Component={Component} {...pageProps} />
</Provider>
</AppProvider>
);
}
}
MyApp.getInitialProps = async ({ ctx }) => {
return {
host: ctx.query.host,
};
};
export default MyApp;
index.js
import { Heading, Page, Button } from "#shopify/polaris";
function Index(props){
async function getProducts(){
const res = await props.axios_instance.get("/products");
return res;
}
async function handleClick() {
const result = await getProducts();
console.log(result);
}
return (
<Page>
<Heading>Shopify app with Node and React </Heading>
<Button onClick={handleClick}>Get Products</Button>
</Page>
);
}
export default Index;
I found the solution for "Error: Failed to parse session token '******' jwt expired" the problem was Computer Time was not synchronized, check the computer time and synchronized it, for my example, I'm on Kali Linux and I search it how to synchronize time on Kali Linux and follow that tutorial when you finally synchronize your time restart your application server and try again. That's it so dump I lost 4 days on this.
I have followed Apollo docs but seem to still be having issues connecting to subscriptions. Here is my code: On the frontend, I can see it trying to connect but is logging:
WebSocket connection to 'ws://localhost:4000/subscriptions' failed:
I have been following this: https://www.apollographql.com/docs/apollo-server/data/subscriptions/ but it seems like the documentation is slightly behind so I may be missing something.
Client:
import ReactDOM from 'react-dom';
import './index.css';
import Routes from './routes';
import 'semantic-ui-css/semantic.min.css';
import { setContext } from '#apollo/client/link/context';
import { WebSocketLink } from '#apollo/client/link/ws';
import { getMainDefinition } from '#apollo/client/utilities';
import {
ApolloProvider,
ApolloClient,
HttpLink,
InMemoryCache,
split,
} from '#apollo/client';
// Http link
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
// Websocket link
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true
}
});
// Attach auth headers to requests
const middlewareLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
const refreshToken = localStorage.getItem('refreshToken');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
x_token: token ? `${token}` : "",
x_refresh_token: refreshToken ? `${refreshToken}`: ""
}
}
});
// Combine
const httpLinkWithMiddleware = middlewareLink.concat(httpLink);
// Split link - either http or ws depending on graphql
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLinkWithMiddleware,
);
// Create client with link
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// Provide client
const App = (
<ApolloProvider client={client}>
<Routes />
</ApolloProvider>
)
// Render
ReactDOM.render(
App,
document.getElementById('root')
);
Server:
import express from 'express';
import path from 'path';
import { fileLoader, mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
import { ApolloServer } from 'apollo-server-express';
import { refreshTokens } from './auth';
import models from './models';
import cors from 'cors';
import jwt from 'jsonwebtoken';
const SECRET = "";
const SECRET2 = "";
const typeDefs = mergeTypes(fileLoader(path.join(__dirname, './schema')));
const resolvers = mergeResolvers(fileLoader(path.join(__dirname, './resolvers')));
const PORT = 4000;
const app = express();
// Cors
app.use(cors('*'));
// Add tokens
const addUser = async (req, res, next) => {
const token = req.headers['x_token'];
if (token) {
try {
const { user } = jwt.verify(token, SECRET);
req.user = user;
} catch (err) {
const refreshToken = req.headers['x_refresh_token'];
const newTokens = await refreshTokens(token, refreshToken, models, SECRET, SECRET2);
if (newTokens.token && newTokens.refreshToken) {
res.set('Access-Control-Expose-Headers', 'x_token', 'x_refresh_token');
res.set('x_token', newTokens.token);
res.set('x_refresh_token', newTokens.refreshToken);
}
req.user = newTokens.user;
}
}
next();
};
app.use(addUser);
// Create server
const server = new ApolloServer({
typeDefs,
resolvers,
subscriptions: {
path: '/subscriptions'
},
context: ({req, res, connection}) => {
const user = req.user;
return { models, SECRET, SECRET2, user };
},
});
// Apply middleware
server.applyMiddleware({ app });
// Sync and listen
models.sequelize.sync({force: true}).then(x => {
app.listen({ port: PORT }, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`);
}
);
});
Any help would be appreciated...
I can't see in the server declaring a websocket creation. Something like:
const WebSocket = require('ws');
const graphQLWebSocket = new WebSocket.Server({noServer: true});
Then also put in:
server.installSubscriptionHandlers(graphQLWebSocket);
After the row with server.applyMiddleware({ app });
More info here.
I'm trying to introducing cls-rtracer and when instantiating it, it's coming as undefined.
The code is written in TypeScript using Object Oriented Paradigm instead of Functional.
Examples posted in NPM for the library are functional.
I've commented the place where it is coming as undefined.
Introducing any other middleware in this express app seems straightforward except cls-rtracer
This is the code:
import express, {NextFunction, Response, Request} from 'express';
import * as bodyParser from 'body-parser';
import helmet from 'helmet';
import morgan from 'morgan';
import cors from 'cors';
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerui from 'swagger-ui-express';
import {Controller} from './api/v1/controllers/Controller';
import {logger} from './api/v1/utils/Logger';
import {errorHandler} from './api/v1/utils/ErrorHandler';
import {BaseError} from './api/v1/utils/BaseError';
import {HttpStatusCode} from './api/v1/constants/HttpStatusCode';
import rTracer from 'cls-rtracer';
export const stream = {
write: (text: string) => {
logger.info(text.replace(/\n$/, ''));
},
};
const swaggerOptions = {
swaggerDefinition: {
info: {
title: 'Backend',
descriptions: 'Service',
contact: {
name: '',
},
servers: ['http://localhost:8080//api/v1/domain'],
version: '1.0.1',
},
},
apis: ['./api/v1/routes/*.ts'],
};
const swaggerDocs = swaggerJSDoc(swaggerOptions);
export class App {
public app: express.Application;
public port: number;
constructor(controllers: Controller[], port: number) {
this.app = express();
this.port = port;
this.initializeMiddlewares();
this.initializeHealth();
this.initializeControllers(controllers);
this.initializeErrorHandler();
this.initializeSwagger();
}
private initializeMiddlewares() {
this.app.use(rTracer.expressMiddleware());
const requestId = rTracer.id();
console.log(requestId); // giving undefined
this.app.use(bodyParser.json());
this.app.use(cors());
this.app.use(
morgan(
[
'ip: :remote-addr',
':method', ':url', 'HTTP/:http-version', 'status: :status',
':res[content-length]', 'referrer: :referrer',
'userAgent: :user-agent', 'responseTime: :response-time ms',
].join(' | '),
{stream: stream},
),
);
this.app.use(express.json());
this.app.use(helmet());
}
private initializeControllers(controllers: Controller[]) {
controllers.forEach((controller) => {
this.app.use('/', controller.router);
});
}
private initializeErrorHandler() {
this.app.use(async (req: Request, res: Response, next: NextFunction) => {
const error = new BaseError('Not Found', HttpStatusCode.NOT_FOUND, true, 'Not Found');
next(error);
});
this.app.use(async (error: Error, req: Request, res: Response, next: NextFunction) => {
if (!errorHandler.isTrustedError(error)) {
// #ts-ignore: Unreachable code error
res.status(error.status).json(error);
}
await errorHandler.handleError(error);
// #ts-ignore: Unreachable code error
res.status(error.httpCode || HttpStatusCode.INTERNAL_SERVER).json({error: error});
});
}
private initializeSwagger() {
this.app.use(
'/api-docs',
swaggerui.serve,
swaggerui.setup(swaggerDocs, {explorer: true}),
);
}
private initializeHealth() { }
public listen() {
this.app.listen(this.port, () => {
logger.info(`listening on the port ${this.port}`);
});
}
}
export default App;
Why does it work flawlessly in the code below:
import express from 'express';
import {CONFIG} from './config';
import MasterRouter from './routers/MasterRouter';
import ErrorHandler from './utils/ErrorHandler';
import rTracer from 'cls-rtracer';
import morgan from 'morgan';
class Server {
public app = express();
public router = MasterRouter;
constructor(
) {
this.correlationalIdMiddleware();
this.loggingMiddleware();
this.routingMiddleware();
this.errorHandlingMiddleWare();
}
correlationalIdMiddleware() {
this.app.use(rTracer.expressMiddleware());
}
loggingMiddleware() {
this.app.use(morgan((tokens, req, res) => {
const requestId = rTracer.id();
return [
`> requestId: ${requestId} -`,
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
tokens.res(req, res, 'content-length'), '-',
tokens['response-time'](req, res), 'ms',
].join(' ');
}));
}
routingMiddleware() {
this.app.use('/api', this.router);
}
errorHandlingMiddleWare() {
this.app.use((req, res, next) => {
const error = new ErrorHandler(404, 'Not Found');
next(error);
});
this.app.use((error: ErrorHandler, req: any, res: any, next: any) => {
const errorObject = {
status: 'error',
statusCode: error.statusCode,
message: error.message,
};
const requestId = rTracer.id();
console.log(`> requestId: ${requestId} - ${JSON.stringify(errorObject)}`);
res.status(error.statusCode || 500).json(errorObject);
});
}
}
const server = new Server;
server.app.listen(CONFIG.PORT, () => {
console.log(`> Server listening on ${CONFIG.PORT}`);
});