Nuxt ServerMiddleware Returning HTML Doc - vue.js

I am building out a webpage which needs to make a call to the Google Geocoder api.
In order to hide the api key from public view, I am trying to set up server middleware to act as a REST api endpoint.
I have checked through all of the documentation and copied all of it, but the response is always the same. I receive the entirety of the html body back from the axios request rather than anything else I send back via express.
In my component I have the following code:
computed: {
normalizedAddress() {
return `${this.member.address.street} ${this.member.address.city}, ${this.member.address.state} ${this.member.address.zip}`.replace(
/\s/g,
'+'
)
}
},
methods: {
async getLocation() {
try {
const res = await axios.get(
`/api/geocode/${this.normalizedAddress}`
)
console.log(res)
} catch (err) {
console.log(err)
}
}
},
In nuxt.config.js I have this setup
serverMiddleware: ['~/api/geocode.js'],
In the root of my project I have an api folder with geocode.js stored there.
geocode.js is below
import express from 'express';
import axios from "axios";
let GEO_API = "MY_API_KEY"
const app = express();
app.use(express.json());
app.get("/", async (req, res) => {
const uri = `https://maps.googleapis.com/maps/api/geocode/json?address=${req.params.address}&key=${GEO_API}`
try {
const code = await axios.get(uri);
if (code.status !== "OK") {
return res.status(500).send(code.status)
}
return res.status(200).send(code);
} catch (err) {
return res.status(500).send(err);
}
});
export default {
path: "/api/geocode/:address",
handler: app
}
Again. The response always has the entire html document from the website sent back (some 100 pages of code).
Even when I set the response to fixed text, that is not sent.
The only detail I can think of that might be interrupting it is that I have my own custom routing setup using the #nuxtjs/router build module in use.

Related

Update Next.js to React 18 breaks my API calls using next-auth

This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233

How to access object on Vue front end from axios proxy

I have a locally hosted mongodb database with mongoose, express, axios, and a Vue front end. Right now I'm trying to access a single object from an exported array, but I'm missing the mark and getting "undefined" as the result.
vue.config.js:
module.exports = {
devServer: {
proxy: 'http://localhost:3000',
}
}
here's the front end Vue script meant to use the objects:
import axios from 'axios';
export default {
name: 'Game',
data () {
return {
pages: [],
currentPage: {},
pageTitle: "",
pageText: "",
options: [],
}
},
created () {
this.getPages();
},
methods: {
async getPages() {
try {
let res = await axios.get('/api/pages');
this.pages = res.data;
console.log(this.pages);
this.currentPage = this.pages[0];
console.log(this.currentPage);
return true;
} catch (error) {
console.log(error);
}
},
my "get" endpoint in pages.js:
router.get('/', async (req, res) => {
try {
let pages = await Page.find();
res.send({pages: pages}); //send result of search for pages as list of pages called "pages"
} catch (error) {
console.log(error);
res.sendStatus(500); //500 = server could not fulfill request
}
});
the route in server.js:
const pages = require('./routes/pages');
app.use('/api/pages', pages);
app.listen(3000, () => console.log('Server listening on port 3000!'));
module.exports = app;
and here's the console output, with the "pages" object from vue's data property and the "currentPage" that's supposed to be at pages[0] (printed to console in earlier example):
I can access the api at 'localhost:3000/api/pages' just fine, but how do I break into that array and access the first page object? I want to get an object from the list axios fetches from mongoose, then hold that object in a variable so I can access it's properties. The whole "pages > [[Target]] > pages > [ ]" is part of the problem I'm sure, but I don't know what to tell the code to open it.
Whoops! I realized my mistake. In pages.js I should have sent "res.send(pages);" After a whole couple days too XD

Nuxt.js - The best place for API calls

I'm new to Vue.js Nuxt and all front-end stuff.
I have a question about API calls. I'm not sure what is the right way, the best practice here.
I have a store. In that store, I have actions that are calling my API and sets state eg.
async fetchArticle({ state, commit }, uuid) {
const response = await this.$axios.get(`articles/${uuid}/`)
commit('SET_ARTICLE', response.data)
},
And that is fine it is working for one component.
But what if I want to just fetch the article and not changing the state.
To be DRY first thing that comes to my mind is to create the service layer that is fetching the data and is used where it is needed.
Is it the right approach? Where can I find some real-world examples that I can take inspiration from?
Using the repository pattern to abstract your API is definitely a good idea! Whether you use the #nuxtjs/axios module or the #nuxt/http module, you can pass either instance to your repository class/function. Below a real world example of an abstracted "repository.js" file.
export default $axios => resource => ({
index() {
return $axios.$get(`/${resource}`)
},
create(payload) {
return $axios.$post(`/${resource}`, payload)
},
show(id) {
return $axios.$get(`/${resource}/${id}`)
},
update(payload, id) {
return $axios.$put(`/${resource}/${id}`, payload)
},
delete(id) {
return $axios.$delete(`/${resource}/${id}`)
}
})
You can then create a plugin to initialize all different kinds of repositories for your endpoints:
import createRepository from '~/path/to/repository.js'
export default (ctx, inject) => {
const repositoryWithAxios = createRepository(ctx.$axios)
const repositories = {
posts: repositoryWithAxios('posts'),
users: repositoryWithAxios('users')
//...
}
inject('repositories', repositories)
}
Further read: Organize and decouple your API calls in Nuxt.js
I will an example of a service layer implementation for my portfolio to create my dashboard that shows some statics about my github and stackoverflow profiles, to do this i created a folder called services inside the project root :
pages
services
|_AxiosConfig.js
|_GitHubService.js
|_StackoverflowService.js
...
in the AxiosConfig.js file i put i created an axios instance with its configuration :
import axios from 'axios';
const clientAPI = url =>
axios.create({
baseURL: url,
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
export default clientAPI;
then in my GitHubService.js i imported that axios instance called clientAPI which i used to my requests :
import clientAPI from './AxiosConfig';
const baseURL = 'https://api.github.com';
export default {
getUser(name) {
return clientAPI(baseURL).get('/users/' + name);
},
getRepos(name){
return clientAPI(baseURL).get('/users/' + name+'/repos');
},
getEvents(name,page){
return clientAPI(baseURL).get('/users/' + name+'/events?per_page=100&page='+page);
},
getLastYearCommits(name,repo){
return clientAPI(baseURL).get('/repos/' + name+'/'+repo+'/stats/commit_activity');
}
};
then in my page i used asyncData hook to fetch my data :
import GitHubService from '../../services/GitHubService'
export default {
...
async asyncData({ error }) {
try {
const { data } = await GitHubService.getUser("boussadjra");
const resRepos = await GitHubService.getRepos("boussadjra");
return {
user: data,
repos: resRepos.data
};
} catch (e) {
error({
statusCode: 503,
message: "We cannot find the user"
});
}
}
I wanted to use axios in my service/service.js file, so instead of passing axios, I accessed it directly like this:
export default {
async fetchArticle() {
let response = await $nuxt.$axios.$get('/api-url')
return response
},
}
In Nuxt, if you want to just get the data without keeping it in your store, you could use the asyncData function, which asynchronously loads data (from API calls and the like) and pushes it into the component's data object before rendering.

NextJS consistently access request object for every page

I'm using express + passport + nextjs to set up an app that will perform authentication using OpenID Connect. The user data is stored on the request object using express-session which gives me req.user on every request as usual.
Now I want to pass the user information to the front-end so that I can use it for something, but there does not seem to be any consistent way to do this for all requests. I can use getServerSideProps for individual pages, but not for every page through either _document or _app. How can I set this up?
Here is my current _document.tsx
import Document, {
Head,
Main,
NextScript,
DocumentContext,
} from "next/document"
export default class Doc extends Document {
public static async getInitialProps(ctx: DocumentContext) {
const req: any = ctx.req
console.log("req/user", `${!!req}/${!!(req && req.user)}`)
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
user: req?.user || "no user",
}
}
public render() {
return (
<html>
<Head />
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
It appears to return a request object only during the very first request, not any subsequent refreshes of the page.
I've created a small repo that reproduces the issue here: https://github.com/rudfoss/next-server-custom-req
It seems ridiculous that there is no way to do this for all pages in an easy manner.
Edit: For reference this is my server.js. It is the only other relevant file in the repo
const express = require("express")
const next = require("next")
const dev = process.env.NODE_ENV !== "production"
const start = async () => {
console.log("booting...")
const server = express()
const app = next({ dev, dir: __dirname })
const handle = app.getRequestHandler()
await app.prepare()
server.use((req, res, next) => {
req.user = {
authenticated: false,
name: "John Doe",
}
next()
})
server.get("*", handle)
server.listen(3000, (err) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log("ready")
})
}
start().catch((error) => {
console.error(error)
process.exit(1)
})
It is recommended to do this via function components, as seen in the Next.js custom App docs:
// /pages/_app.tsx
import App, { AppProps, AppContext } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (appContext: AppContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext)
const req = appContext.ctx.req
return {
pageProps: {
...appProps.pageProps,
user: req?.user,
},
}
}
As in your answer, this will run on every request though so automatic static optimization will not be active.
Try a demo of changing pageProps in MyApp.getInitialProps (without usage of req.user) on the following CodeSandbox:
https://codesandbox.io/s/competent-thompson-l9r1u?file=/pages/_app.js
Turns out I can override getInitialProps on _app to make this work:
class MyApp extends App {
public static async getInitialProps({
ctx
}: AppContext): Promise<AppInitialProps> {
const req: any = ctx.req
return {
pageProps: {
user: req?.user
}
}
}
public render() {
//...
}
}
This will run on every request though so static optimization will not work, but in my case I need the information so I'm willing to accept the trade-off.
Edit: This answer also works, but it uses the "old" class-based component syntax which is no longer recommended. See answer from Karl for a more modern version using functional-component syntax.
I also had the similar problem where I had to fetch loggedIn user details from my Auth api. I solved it by wrapping my whole app inside a context provider, then using a set function for the initialState, which will remember if it was called before and fetch user details only once. Then in my each page, wherever I require these user details, I used the context to see if details are available and call the set function if details are not available. This way I think I achieved:
Only one request to fetch user details
Because it happens from the client side, TTFB is better
I can still take advantage of getStaticProps and getServerSideProps where it is required.

v-on:click never firing on button in Nuxt component, because of middleware

I've seen this elsewhere in other questions, but haven't seen a solution. I'm trying to make a simple GDPR cookie notification that closes on click, using a button. I'm in Universal mode, meaning mounted() isn't available, so I'm trying to set cookies through Vuex. However, the click event I have bound to the button in my component isn't firing.
Edit: After building a codesandbox version of my app, which worked as it should, I went through and hacked up my Nuxt app until I found what was causing the problem. As it turns out, it was my middleware, and more specifically, the fact that I was using the fs-extra library to read JSON files. Still not clear on why this is happening, so any suggestions are welcome. The code below includes my middleware.
components/CookieNotice.vue
<template>
<div v-if="cookie != true" class="cookie_notice_wrap">
<div class="cookie_notice">
<p class="notice_message">This site uses cookies for analytics purposes.</p>
<button #click.prevent="dismissNotification" class="notice_dismiss">Close</button>
</div></div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "CookieNotice",
methods: {
dismissNotification: function(e) {
console.log("we clicked?");
document.querySelector(".cookie_notice_wrap").classList.add("hidden_click");
this.store.dispatch("cookieStateChange", true);
}
},
computed: {
...mapGetters(["cookie"]),
}
}
</script>
Actions from store/index.js
export const actions = {
async getPosts({ state, commit, dispatch }) {
if (state.posts.length) {
return
}
try {
await axios.get("/api/json-access");
}
catch (err) {
console.log(err);
}
},
nuxtServerInit({ state, commit, dispatch }, { req }) {
dispatch("getPosts");
const seen = this.$cookies.get("cookie_notice_seen");
if (!seen) {
dispatch("cookieStateChange", false);
}
},
cookieStateChange({state, commit, dispatch}, bool) {
// this does set the cookie correctly, unsure if it will work the same when bound to the button click
commit("updateCookie", bool);
this.$cookies.set("cookie_notice_seen", bool, {
path: "/",
maxAge: 60 * 60 * 24 * 7
});
}
}
~/middleware/api/json-access.js:
const fse = require('fs-extra');
import axios from 'axios';
const storeLocation = "middleware/full_site.json";
export default async function({ store, route, redirect }) {
const exists = await fse.pathExists(storeLocation);
let posts;
if (!exists) {
await fse.ensureFile(storeLocation);
posts = await postsFromWP();
fse.writeJSON(storeLocation, posts);
}
else {
try {
posts = await fse.readJSON(storeLocation);
}
catch (err) {
console.log(err);
}
}
store.commit("updatePosts", posts);
}
async function postsFromWP() {
const url = ".../example/json/file.json";
const config = { headers: { "Accept": "application/json" }};
let posts = await axios.get(url, config);
posts = posts.data
.filter(el => el.status === "publish")
.map(({ id, slug, title, excerpt, date, tags, content }) => ({
id, slug, title, excerpt, date, tags, content
}));
return posts;
}
I had the middleware configured in nuxt.config.js via routes -> middleware before, but currently have it set to go through serverMiddleware instead, for testing. I also added the action that triggers getting the posts, in case that's also part of it. This has definitely hit on a limit of my Nuxt/Vue understanding - I have no idea why this could be happening, so any wisdom is much appreciated.
If you wind up dealing with something similar, fs or fs-extra are your culprits. You can't use file operations on the client side, which is when these middleware actions are happening (I think). The cookie notice only fully worked when I removed the fs-extra import at the very top of the middleware file.