I am making a shopify app and have created a Koa + NodeJS backend and NextJS running in frontend, in the same port, and also I have created a custom server.js file When running locally in my PC, its running all right, all the routes work as expected, and I am able to fetch data from Frontend (React) from the routes defined in my backend (Koa).
Now the problem is that, when I deploy my app to vercel it doesn't seem to be recognizing my Koa routes, and is throwing a 404 error in the console for all the requests I make to the backend routes. This is my first time working with NextJS, so I have really very little idea on what is wrong here, so I would like some support on this please.
Also when I deploy it, the shopify auth also doesn't seem to be working anymore like it does when its running in local development.
My Code:
server.js:
require('isomorphic-fetch');
const dotenv = require('dotenv');
dotenv.config();
const Koa = require('koa');
const next = require('next');
const { default: createShopifyAuth } = require('#shopify/koa-shopify-auth');
const { verifyRequest } = require('#shopify/koa-shopify-auth');
const session = require('koa-session');
const { default: graphQLProxy } = require('#shopify/koa-shopify-graphql-proxy');
const { ApiVersion } = require('#shopify/koa-shopify-graphql-proxy');
const Router = require('koa-router');
const { receiveWebhook, registerWebhook } = require('#shopify/koa-shopify-webhooks');
const getSubscriptionUrl = require('./server/getSubscriptionUrl');
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const {
SHOPIFY_API_SECRET_KEY,
SHOPIFY_API_KEY,
HOST,
} = process.env;
app.prepare().then(() => {
const server = new Koa();
const router = new Router();
server.use(session({ sameSite: 'none', secure: true }, server));
server.keys = [SHOPIFY_API_SECRET_KEY];
server.use(
createShopifyAuth({
apiKey: SHOPIFY_API_KEY,
secret: SHOPIFY_API_SECRET_KEY,
scopes: ['read_products', 'write_products'],
async afterAuth(ctx) {
const { shop, accessToken } = ctx.session;
ctx.cookies.set("shopOrigin", shop, {
httpOnly: false,
secure: true,
sameSite: 'none'
});
const registration = await registerWebhook({
address: `${HOST}/webhooks/products/create`,
topic: 'PRODUCTS_CREATE',
accessToken,
shop,
apiVersion: ApiVersion.October19
});
if (registration.success) {
console.log('Successfully registered webhook!');
} else {
console.log('Failed to register webhook', registration.result);
}
await getSubscriptionUrl(ctx, accessToken, shop);
}
})
);
router
.get('/api', ctx => {
ctx.res.statusCode = 200;
ctx.body = "API Route"
})
const webhook = receiveWebhook({ secret: SHOPIFY_API_SECRET_KEY });
router.post('/webhooks/products/create', webhook, (ctx) => {
console.log('received webhook: ', ctx.state.webhook);
});
server.use(graphQLProxy({ version: ApiVersion.April19 }));
router.get('*', verifyRequest(), async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
});
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
I found out that Vercel isn't supporting custom servers anymore, if I right. from this https://github.com/vercel/next.js/issues/9397#issuecomment-556215227
So, I used Heroku to deploy my app instead, and now it is working all right.
Related
hope you are good,
I am building my next.js weather app project using openweather API, I set up my proxy server, so the API key won't appear on the client side, it works perfect on my localhost, but when I deploy it on vercel or heroku, it returns error 404, it does the same when requesting directly on postman or browser.
PS: the environment variables are set in my vercel dashboard.
PS: api url example localhost:3000/api/weather?q=london
my server.js
const next = require('next');
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const needle = require('needle');
const url = require('url');
const cors = require('cors');
app.prepare().then(() => {
const API_BASE_URL = process.env.API_BASE_URL;
const API_KEY_NAME = process.env.API_KEY_NAME;
const API_KEY_VALUE = process.env.API_KEY_VALUE;
const server = express();
server.use(
cors({
origin: '*',
})
);
server.get(
'/api/:stat',
async (req, res) => {
try {
const params = new URLSearchParams({
...url.parse(req.url, true).slashes,
...url.parse(req.url, true).query,
[API_KEY_NAME]: API_KEY_VALUE,
});
const apiRes = await needle(
'get',
`${API_BASE_URL}/${req.params.stat}?${params}`
);
const data = apiRes.body;
res.status(200).json(data);
} catch (error) {
res.status(500).json({ error });
}
}
);
server.all('*', (req, res) => {
return handle(req, res);
});
server.listen(port, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
});
When scaffolding a Nuxt app using create-nuxt-app, there's an option to add both a backend server and a testing framework.
I've chosen Express as by backend framework, and Jest as my testing framework, which allows me to runs tests with Jest perfectly. However, there's no server-side test example available in which you can test API end points.
I've create an api endpoint /api/threads and tried testing it with something like this:
const request = require('supertest')
const app = require('../app')
describe('GET /api/threads', () => {
it('should return 200', async () => {
await request(app)
.get(`/api/threads`)
.expect(200)
})
})
But am returned with the error: VueRenderer is not a constructor
I also made sure to export app.js, which currently looks like:
require('dotenv-safe').load()
const path = require('path')
const express = require('express')
const consola = require('consola')
const bodyParser = require('body-parser')
const bcrypt = require('bcryptjs')
const session = require('express-session')
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const SequelizeStore = require('connect-session-sequelize')(session.Store)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
const { Nuxt, Builder } = require('nuxt')
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
const sequelize = require('./sequelize')
const models = require('./models')
const router = require('./router')
const controllers = require('./controllers')
const app = express()
config.dev = !(
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test'
)
function addSessions() {
const sessionStorage = new SequelizeStore({
db: sequelize
})
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: sessionStorage
})
)
app.use(passport.initialize())
app.use(passport.session())
sessionStorage.sync()
}
function addLocalStrategy() {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password'
},
async (email, password, done) => {
try {
let user = await models.User.findOne({ where: { email } })
if (!user)
return done(null, false, {
message: 'Incorrect email.'
})
user = user.toJSON()
const passValid = await bcrypt.compare(
password,
user.password.toString('utf8')
)
if (passValid === false)
return done(null, false, {
message: 'Incorrect password.'
})
if (user && user.subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(
user.subscriptionId
)
user.subscriptionStatus = subscription.status
}
return done(null, user)
} catch (err) {
console.log(err)
return done(err)
}
}
)
)
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser(async (id, done) => {
try {
const user = await models.User.findById(id)
done(null, user)
} catch (err) {
throw new Error(err)
}
})
}
async function start() {
try {
const nuxt = new Nuxt(config)
const { host, port } = nuxt.options.server
// Build only in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
await nuxt.ready()
}
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
addSessions()
addLocalStrategy()
app.use('/', router)
app.use('/private', controllers.User.authenticate)
app.use('/private', express.static(path.join(__dirname, 'private')))
// Give nuxt middleware to express
app.use(nuxt.render)
app.listen(port, host)
consola.ready({
message: `Server listening on http://${host}:${port}`,
badge: true
})
} catch (err) {
throw new Error(err)
}
}
start()
module.export = app
Essentially it's the scaffolded server/app.js, but with code for sessions and authentication.
Any ideas on how to successfully receive a 200 response when hitting a backend API endpoint with a Nuxt / Express combo?
Once you create an instance of Nuxt which you store inside the nuxt variable. you can try printing it out to see whats there. The nuxt.server.app is the object you can feed to supertest in order to test API endpoints.
const { resolve } = require('path')
const { Nuxt, Builder } = require('nuxt')
const request = require('supertest')
// We keep the nuxt and server instance
// So we can close them at the end of the test
let nuxt = null
// Init Nuxt.js and create a server listening on localhost:4000
beforeAll(async () => {
const config = {
dev: process.env.NODE_ENV === 'production',
rootDir: resolve(__dirname, '../'),
mode: 'universal',
}
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
await nuxt.server.listen(3000, 'localhost')
}, 30000)
// Example of testing only generated html
describe('GET /', () => {
test('Route / exits and render HTML', async () => {
const { html } = await nuxt.renderRoute('/', {})
expect(html).toContain('welcome')
})
})
describe('GET /', () => {
test('returns status code 200', async () => {
const response = await request(nuxt.server.app).get('/')
expect(response.statusCode).toBe(200)
})
})
describe('GET /test', () => {
test('returns status code 404', async () => {
const response = await request(nuxt.server.app).get('/test')
expect(response.statusCode).toBe(404)
})
})
// Close server and ask nuxt to stop listening to file changes
afterAll(() => {
nuxt.close()
})
I want to use Cloud Functions for Firebase to deploy the React application for shopify app.
I am new to both Next and Koa.
Based on this repo the below code is how to host a simple react application in Firebase.
const path = require('path')
const functions = require('firebase-functions')
const next = require('next')
var dev = process.env.NODE_ENV !== 'production'
var app = next({
dev,
conf: { distDir: `${path.relative(process.cwd(), __dirname)}/next` }
})
var handle = app.getRequestHandler()
exports.next = functions.https.onRequest((req, res) => {
console.log('File: ' + req.originalUrl) // log the page.js file that is being requested
return app.prepare().then(() => handle(req, res))
})
Which works correctly, no issue.
Then based on this tutorial from shopify I need to integrate koa and other dependencies in server.js, which in my case I believe it should be placed inside the firebase function. so I get to this code
const path = require('path')
const isomorphicFetch = require('isomorphic-fetch');
const Koa = require('koa');
const functions = require('firebase-functions')
const next = require('next');
const ShopifyConfig = require('./shopify.js');
const { default: createShopifyAuth } = require('#shopify/koa-shopify-auth');
const dotenv = require('dotenv');
const { verifyRequest } = require('#shopify/koa-shopify-auth');
const session = require('koa-session');
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 3000;
var dev = process.env.NODE_ENV !== 'production'
var app = next({
dev,
conf: { distDir: `${path.relative(process.cwd(), __dirname)}/next` }
})
var handle = app.getRequestHandler()
const server = new Koa();
server.use(session(server));
server.keys = [ShopifyConfig.secretKey];
server.use(
createShopifyAuth({
apiKey: ShopifyConfig.key,
secret: ShopifyConfig.secretKey,
scopes: [],
afterAuth(ctx) {
const { shop, accessToken } = ctx.session;
ctx.redirect('/');
},
}),
);
server.use(verifyRequest());
server.use(async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
});
exports.next = functions.https.onRequest((req, res) => {
console.log('File: ' + req.originalUrl) //
// This is old code
// return app.prepare().then(() => {
// handle(req, res);
// })
// I tried this #1
// server.callback(req, res);
})
// I tried this #2
// exports.next = functions.https.onRequest(server.callback);
// I tried this #3
// exports.next = functions.https.onRequest(server.callback());
// I tried this #4
exports.next = functions.https.onRequest((req, res) => {
console.log('File: ' + req.originalUrl)
return app.prepare().then(() => {
server.callback(req, res);
//handle(req, res);
})
})
My question is now based on Koa what code should be in functions.https.onRequest ? please note there is no code to listen to port since it doesn't make sense for firebase functions.
I tried #1, #2, #3, as well as this post
1 -> I get request timeout
2 -> I get request timeout
3 -> I get "Cannot access middleware of undefined"
4 -> I get request timeout
Thanks to kvindasAB
server.callback is not the callback by itself, rather a function that generates the callback from your configuration I assumed.
so code needs to be changed to
server.callback()(req, res);
const functions = require('firebase-functions');
const app = new Koa();
...
exports['http'] = functions.https.onRequest(app.callback());
I've got some issues with authentication (using cookies and session) and my electron-app
The use case:
User logs in
Session created and cookie is stored. (by app-bl module)
I read about electron-session and electron-cookies (https://electronjs.org/docs/all?query=coo#class-cookies) but nothing works.
Application structure:
electron-app
---express-app
------app-bl
------react-client
Electron version: 3.0.13
I used this to use express within electron:
https://github.com/frankhale/electron-with-express
It seems like electrons main process doesn't know about cookies created by the rendered process.
electron/main.js:
const electron = require('electron');
const { app, BrowserWindow, session } = electron
let mainWindow;
function createWindow() {
const screenElectron = electron.screen;
mainWindow = new BrowserWindow({
show: false,
autoHideMenuBar: true,
icon: `${__dirname}/assets/icon.ico`
});
mainWindow.webContents.openDevTools();
mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.on("close", () => {
mainWindow.webContents.send("stop-server");
});
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
mainWindow.on("closed", () => {
mainWindow = null;
});
}
express-app/index.js:
const ev = require('express-validation');
const Path = require('path')
const Express = require('express')
const BodyParser = require('body-parser')
const CookieParser = require('cookie-parser');
const Session = require('express-session');
const App = require('./app/index.js')
// Init server
const express = Express()
const router = Express.Router()
const port = parseInt(process.argv[2]) || process.env.PORT || 5001
const ip = "0.0.0.0"
express.use(BodyParser.urlencoded({ extended: true }))
express.use(BodyParser.json())
express.use(CookieParser())
express.use(Session({
key: 'sessionId',
secret: 'key',
resave: false,
saveUninitialized: false,
cookie: {
expires: 600000
}
}))
// Init Application
const app = App({ express, router })
// Static content
express.use(Express.static(Path.join(__dirname, './client/dist')))
express.use('/*', Express.static(Path.join(__dirname, './client/dist/index.html')))
// Error handler
express.use(function (err, req, res, next) {
console.log(err)
if (err instanceof ev.ValidationError) {
return res.status(err.status).json({
status: err.status,
message: err.statusText
});
}
return res.status(err.status).json({
status: err.status,
message: err.message
});
});
(async () => {
try {
await app.init()
const server = await app.start(ip, port);
console.log("Server started http://%s:%s", ip, port)
} catch (e) {
console.log(e);
process.exit(1);
}
})()
And this is how I'm creating the session after successful login in app-bl module:
async function loginHandler(req, res, next) {
const username = req.body.username
const password = req.body.password
try {
const user = await authService.login(username, password)
req.session.userId = user.id;
res.json({ user })
} catch (error) {
error.status = 500
next(error)
}
}
I managed to create cookies inside main process and I can see then using console.log, but nothing is showing inside devTools, I tried this code:
const mainSession = mainWindow.webContents.session
const cookie = {
url: 'http://localhost:8000',
name: 'sessionId',
domain: 'localhost',
expirationDate: 99999999999999
}
mainSession.cookies.set(cookie, (error) => {
if (error) {
console.error(error)
}
})
mainSession.cookies.get({}, (error, cookies) => {
console.log(cookies)
})
I have the feeling I'm missing something here.
This package works: https://github.com/heap/electron-cookies
It seems to use Local Storage to emulate cookies. So you can read/write cookies using normal JavaScript syntax, and hopefully your application won't know the difference.
New to React/Node.
I have an implementation of React(React-boiler-plate with webpack etc)/Node running on the same host on Heroku.
I am using passport and twitter-oath sessions.
When I hit the endpoint http://example.heroku.com/auth/twitter/callback everything works accordingly (as well as running local dev server).
When I try to access it via HTTPS https://example.heroku.com/auth/twitter/callback React intercepts it and shows a page not found page.
I am trying to get an understanding to understand why this happens and the best way to handle this in a "production" like environment. I would like to be handle /auth/twitter and /auth/twitter/callback all on the same host.
I have tried adding http proxy in misc places as well as package.json and to no avail I am spinning my wheels.
Thank you in advance.
auth routes
module.exports = app => {
app.get('/api/logout', (req, res) => {
// Takes the cookie that contains the user ID and kills it - thats it
req.logout();
// res.redirect('/');
res.send(false);
// res.send({ response: 'logged out' });
});
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get(
'/auth/twitter/callback',
passport.authenticate('twitter', {
failureRedirect: '/'
}),
(req, res) => {
res.redirect('/dashboard');
}
);
app.get('/api/current_user', (req, res) => {
// res.send(req.session);
// res.send({ response: req.user });
res.send(req.user);
});
};
index.js
app.use(morgan('combined'));
app.use(bodyParser.json());
app.use(
//
cookieSession({
// Before automatically expired - 30 days in MS
maxAge: 30 * 24 * 60 * 60 * 1000,
keys: [keys.COOKIE_KEY]
})
);
app.use(passport.initialize());
app.use(passport.session());
require('./routes/authRoutes')(app);
// They export a function - they turn into a function - then immediately call with express app object
app.use('/api/test', (req, res) => {
res.json({ test: 'test' });
});
setup(app, {
outputPath: resolve(process.cwd(), 'build'),
publicPath: '/',
});
// get the intended host and port number, use localhost and port 3000 if not provided
const customHost = argv.host || process.env.HOST;
const host = customHost || null; // Let http.Server use its default IPv6/4 host
const prettyHost = customHost || 'localhost';
/ Start your app.
app.listen(port, host, async err => {
if (err) {
return logger.error(err.message);
}
// Connect to ngrok in dev mode
if (ngrok) {
let url;
try {
url = await ngrok.connect(port);
} catch (e) {
return logger.error(e);
}
logger.appStarted(port, prettyHost, url);
} else {
logger.appStarted(port, prettyHost);
}
});
console.log('Server listening on:', port);
/**
* Front-end middleware
*/
module.exports = (app, options) => {
const isProd = process.env.NODE_ENV === 'production';
if (isProd) {
const addProdMiddlewares = require('./addProdMiddlewares');
addProdMiddlewares(app, options);
} else {
const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
const addDevMiddlewares = require('./addDevMiddlewares');
addDevMiddlewares(app, webpackConfig);
}
return app;
};
const path = require('path');
const express = require('express');
const compression = require('compression');
module.exports = function addProdMiddlewares(app, options) {
// messing around here
const proxy = require('http-proxy-middleware');
const apiProxy = proxy('/api', { target: 'http://localhost:3000' });
const apiProxy2 = proxy('/auth', { target: 'http://localhost:3000' });
app.use('/api', apiProxy);
app.use('/auth/*', apiProxy2);
const publicPath = options.publicPath || '/';
const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
// compression middleware compresses your server responses which makes them
// smaller (applies also to assets). You can read more about that technique
// and other good practices on official Express.js docs http://mxs.is/googmy
app.use(compression());
app.use(publicPath, express.static(outputPath));
app.get('*', (req, res) =>
res.sendFile(path.resolve(outputPath, 'index.html')),
);
};
const path = require('path');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
function createWebpackMiddleware(compiler, publicPath) {
return webpackDevMiddleware(compiler, {
logLevel: 'warn',
publicPath,
silent: true,
stats: 'errors-only',
});
}
module.exports = function addDevMiddlewares(app, webpackConfig) {
const compiler = webpack(webpackConfig);
const middleware = createWebpackMiddleware(
compiler,
webpackConfig.output.publicPath,
);
app.use(middleware);
app.use(webpackHotMiddleware(compiler));
// Since webpackDevMiddleware uses memory-fs internally to store build
// artifacts, we use it instead
const fs = middleware.fileSystem;
app.get('*', (req, res) => {
fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
if (err) {
res.sendStatus(404);
} else {
res.send(file.toString());
}
});
});
};
Chances are you have a service worker that is running client side and intercepting the requests, then serving your react app from it's cache.
One hint that gives it away is that the service worker will only be installed / run over https https://developers.google.com/web/fundamentals/primers/service-workers/#you_need_https
Solution would be to either edit the service worker code to have it not serve for the auth urls or disable it all together if you are not planning to build an app around it, it may be more trouble than it is worth.