How to use a private API key with Nuxt (on the client)? - vue.js

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},

Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585

Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)

UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

Related

404 error calling GET API from a shopify theme-extension

I'm new to shopify development, and can't figure out how to call an authenticated API from a shopify theme-extension. Essentially, I'm trying to make a theme extension, where one of the functionalities is that when a checkbox is clicked, an API that counts the number of products is called.
I have a working api that gets the product count, and in web>index.js, I have set-up the end-point:
app.get("/api/products/count", async (_req, res) => {
const countData = await shopify.api.rest.Product.count({
session: res.locals.shopify.session,
});
res.status(200).send(countData);
});
Under web>frontend>hooks, I have the authenticated hooks set-up as shown below. I've tested that if I call the "api/products/count" API from one of the web pages using useAppQuery, it works as expected, and returns the product count.
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useMemo } from "react";
import { useQuery } from "react-query";
export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => {
const authenticatedFetch = useAuthenticatedFetch();
const fetch = useMemo(() => {
return async () => {
const response = await authenticatedFetch(url, fetchInit);
return response.json();
};
}, [url, JSON.stringify(fetchInit)]);
return useQuery(url, fetch, {
...reactQueryOptions,
refetchOnWindowFocus: false,
});
};
In my theme extension code, I've added an event listener to the checkbox which calls getProductCount. In getProductCount, I want to call /api/products/count:
import { useAppQuery } from "../../../web/frontend/hooks";
export const getProductCount = (product) => {
const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: "/api/products/count",
reactQueryOptions: {
onSuccess: () => {
setIsLoading(false);
},
},
});
}
However, when I run locally and click the checkbox, it returns a 404 error trying to find useAppQuery. The request URL is https://cdn.shopify.com/web/frontend/hooks. It seems like the authentication isn't working because that URL looks incorrect.
Am I missing a step that I need to do in order to call an authenticated API from a theme-extension?
I thought the issue was just the import path for useAppQuery but I've tried different paths, and they all return the same 404 issue.
If you want a hot tip here. In your theme App extension, you do not actually need to make an API call to get a product count. In your theme app extension, you can just use Liquid, and dump the product count out to a variable of your choice, and use the count, display the count, do whatever.
{{ shop.product_count }}
Of course, this does not help you if you need other storefront API calls in your theme App extension, but whatever. In my experience, I render the API Access Token I need in my theme app extension, and then making my Storefront API calls is just a fetch().
The only time I would use authenticated fetch, is when I am doing embedded App API calls, but that is a different beast from a theme app extension. In there, you do not get to make authenticated calls as the front-end is verboten for those of course. Instead you'd use App Proxy for security.
TL:DR; Storefront API calls with a token should not fail with a 404 if you call the right endpoint. You can use Storefront API inside a theme app extension. Inside a theme app extension, if you need backend Admin API access, you can use App Proxy calls.

React Query uses outdated headers (old JWT token) after Keycloak refresh token

I'm pretty new in React-Native programming, but here is the context.
We are using React Query and Axios libraries in our project. As AuthManager we are using Keycloak and for the library managing auth status we have React Native Keycloak. We encounter a tedious problem with our server responding randomly 401 at our requests after a certain amount of time, bringing also to the app crash sometimes.
We reproduced the error making the Bearer Token of Keycloak expire after only 1 minute. This caused almost immediatly the 401 error and we wondered why this is happening.
Let's say we have a screen with some "Activities" and this screen is the first thing the user will see. For handling requests, in our code we use some custom hooks that reference useQuery, for example:
export function useActivities(): UseQueryResult<ActivityList> {
const { headers } = useHeaders();
return useQuery(
['activities', today.start],
() => getActivitiesList(headers), // Note 1
{
enabled: !!today.start,
}
);
}
The important point of it is that we useHeaders to get our updated headers with the Keycloak token and our realm settings. useHeaders is almost everywhere in our app.
export function useHeaders(): UseHeaders {
const { keycloak } = useKeycloak();
const KEYCLOAK_REALM = remoteConfig().getString('KEYCLOAK_REALM');
const headers = {
Authorization: `Bearer ${keycloak?.token}`,
Realm: KEYCLOAK_REALM,
};
return { headers };
}
Now, the getActivitiesList is simple as five:
async function getActivitiesList(headers: UseHeaders['headers']): Promise<ActivityList> {
const url = `${BASE_URL}${VERSION}/activities/grouped?end=${end}&start=${start}`;
// Note 2
return axios
.get(url, { headers })
.then((res) => res.data)
.catch((e) => console.error('Error fetching grouped activities:', e));
}
The problem with all of that is that whenever Keycloak will trigger the refresh token, the token inside keycloak object is changed, the headers inside useActivities are changed BUT if I print the headers inside my getActivitiesList (// Note 2), or even inside the query function (// Note 1), headers will not be updated. Sometimes it just causes to make two requests (one that fails and show error, the other one actually working), some other times the app crashes without any explain. This makes me wonder why the query function will not update its headers and passed the "old" headers inside getActivitiesList.
For now we are mitigating this problem in two different points.
After keycloak init, we pass immediatly to axios a global header with axios.defaults.headers.common.Realm = KEYCLOAK_REALM;
After receiving a valid token from Keycloak, we overwrite the Authorization header with a new one: axios.defaults.headers.common.Authorization = 'Bearer ${keycloak?.token}';
This is not a perfect solution and we are here to search some info about this problem.
Someone get something similar? How to manage the token refresh in useQuery function?

How to use Nuxt $auth inside an axios plugin (How to add Token to all axios requests)

Im looking to use $auth inside my Nuxt project, specially inside an axios plugin.
Here is my code:
plugins/api.js
export default function ({ $axios }, inject) {
const api = $axios.create({
headers: {
common: {
Accept: 'text/plain, */*',
},
},
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Now the problem comes when I try to use $auth from #nuxtjs/auth-next package.
As stated in the docs:
This module globally injects $auth instance, meaning that you can
access it anywhere using this.$auth. For plugins, asyncData, fetch,
nuxtServerInit and Middleware, you can access it from context.$auth.
I tried the following:
This results in $auth being undefined
export default function ({ $axios, $auth }, inject) {
This one was near
export default function ({ $axios, app }, inject) {
console.log(app) //This one logs $auth in the object logged
console.log(app.$auth) // I don't understand why but this one returns undefined
My main goal here is to make use of this.$auth.strategy.token.get()and pass it (if the token exists of course) to the headers of every request made using this.$api
I have been looking for similar questions and answers but none has helped me to solve this, I could just add the token every time I write this.$api but that would increase the code unnecessarily.
Thanks in advance to all the people for your time and help.
EDIT:
Okay, now I made a test. and the next code is actually logging the $auth object correctly, it seems some time is needed to make it work but now Im afraid that using setTimeout could cause an error because I can't know exactly how much time is needed for $auth to be available.
export default function ({ $axios, app }, inject) {
setTimeout(() => {
console.log('After timeout', app.$auth)
}, 50)
EDIT 2:
So now I have made more tests, and using 0 milliseconds instead of 50 works too, so I will use setTimeout with 0 milliseconds for now, I hope anyone find a better solution or explain why $auth is not available before using setTimeout so I can decide what to do with my code.
EDIT 3:
After trying to wrap all my previous code inside setTimeout I noticed that the code fails, so that isn't a solution.
I have found a solution so I will post it so that every person that could have the same problem in the future can solve it.
It turns out that I could easily solve it using interceptors.
export default function ({ $axios, app }, inject) {
// At this point app.$auth is undefined. (Unless you use setTimeout but that is not a solution)
//Create axios instance
const api = $axios.create({
headers: {
common: {
Accept: 'application/json', //accept json
},
},
})
// Here is the magic, onRequest is an interceptor, so every request made will go trough this, and then we try to access app.$auth inside it, it is defined
api.onRequest((config) => {
// Here we check if user is logged in
if (app.$auth.loggedIn) {
// If the user is logged in we can now get the token, we get something like `Bearer yourTokenJ9F0JFODJ` but we only need the string without the word **Bearer**, So we split the string using the space as a separator and we access the second position of the array **[1]**
const token = app.$auth.strategy.token.get().split(' ')[1]
api.setToken(token, 'Bearer') // Here we specify the token and now it works!!
}
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Also Nuxt Auth itself has provided a solution for this issue:
https://auth.nuxtjs.org/recipes/extend/

Apollo client + Next.js - Adding Authorization token to client request

GOAL
I want to populate my Authorization header with an access token. I want to store that access token in the Apollo cache as Auth0 explicitly state not to store access tokens in local storage (I don't know why the Apollo Client docs seem to think it's okay).
Failing that, I want to store my access token securely and be able to add it to each Apollo Client request to the Apollo Server.
const apolloClient = withApollo(({ctx, headers, initialState}) => {
const cache = new InMemoryCache();
// Code here to access local cache.
return new ApolloClient({
cache,
link: new HttpLink({
uri: <apollo server endpoint>,
headers: {
Authorization: `Bearer ${accessToken}`,
'Access-Control-Allow-Origin': 'http://localhost:3000/',
...headers
},
credentials: 'include'
}),
typeDefs,
resolvers
})
})
class MyApp extends App {
render() {
const { Component, pageProps, apollo } = this.props;
return (
<ApolloProvider client={apollo}>
<Component {...pageProps} />
</ApolloProvider>
);
}
}
export default apolloClient(MyApp);
TRIED
I've tried to access localStorage from within the withApollo function but it is SSR'd so I'm unable to access that. I was going to use the localStorage to set a boolean to check in the withApollo function so it'll know the access token has been added to the apollo cache after the redirect.
I've tried just using the cache with a key storing a boolean to check if the user has signed in which is set in the same function where the access token. If that is true I'd access the access token key and add that to the Authorization header. But I was getting issues with setting the initial state overriding the cache set in sign in function.
OTHER IDEAS
I thought I could pass the access token through in the ctx argument, but I'm not familiar enough with Next.js to figure out if that's valid way to do it.
I thought I could pass in a props to the withApollo function from a component, but that doesn't seem like it'd be possible.
QUESTIONS
What is the best way to store the access key so as to add it to the Apollo Client with each request?
I noticed some people are using a fetch polyfill, would that be appropriate for this situation? If so, how would that work?
How does the withApollo HOC work and why is it needed to work with Next.js? I've read some of the code behind this, but I don't fundamentally understand why it is needed.
I was able to find a solution to my question. I just didn't fully understand the Apollo Client and how to use all of the required packages.
SOLUTION
I used setContext from the apollo-link-context library. It's part of the link set of libraries that Apollo Client offers to customise the network request after a graphql operation has started. I set the header in the setContext function. Like so:
const authLink = setContext((_, { headers }) => {
// It is also possible to use the local storage method in here.
const data = cache.readQuery({
query: ACCESS_TOKEN
});
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${data.accessToken}` : ""
}
}
});
The following is outside the withApollo function.
const httpLink = new HttpLink({
uri: '<server uri>',
});
The following is inside the withApollo function.
return new ApolloClient({
cache,
link: authLink.concat(httpLink),
typeDefs,
resolvers
})
The docs for how setContext are here and the docs for the `apollo-link-context' are here.
I think I have a better solution so I'm gonna post it over here. I guess you want to configure your client properly instead of passing the token to each query. I remembered how I used react-query's client. You can do the same here.
My client looks like this:
import { ApolloClient, InMemoryCache } from '#apollo/client'
export const client = new ApolloClient({
uri: 'https://api.github.com/graphql',
headers: {
Authorization: `bearer ${localStorage.getItem('personal-access-token')}`
},
cache: new InMemoryCache()
})
If I don't have a token in local storage it'll be set to 'bearer null'. But after I get a new token you should get the Apollo Client's instance and edit Authorization header like this:
import { useApolloClient } from '#apollo/client'
const setToken = token => {
client.link.options.headers.Authorization = `bearer ${token}`
localStorage.setItem('personal-access-token', token)
}
I hope this helps someone.
P.S. Make sure to wrap the component which alters the Apollo Client's instance with ApolloProvider.

How to minimize data traffic in vuejs

At work, we think about using Vuejs with Vuex for our frontend. I have already used it for private projects.
One of our big questions is the traffic. Our platform needs a lot of data, sometimes in large packages. Some of them are really big and it would be more expensive to load them more than once.
I've seen many examples of vue with vuex, but for me it looked like all the samples would request the same (already loaded) data every time they paged.
My real question is: Is there a way in vuejs or general to avoid or solve this problem? The only helpful thing I have found so far was this.
As far as I know, you can use this library https://github.com/kuitos/axios-extensions
An example here
import Axios from 'Axios';
import { throttleAdapterEnhancer } from 'axios-extensions';
const http = axios.create({
baseURL: '/',
headers: { 'Cache-Control': 'no-cache' },
adapter: throttleAdapterEnhancer(axios.defaults.adapter, { threshold: 2 * 1000 })
});
http.get('/users'); // make real http request
http.get('/users'); // responsed from the cache
http.get('/users'); // responsed from the cache
setTimeout(() => {
http.get('/users'); // after 2s, the real request makes again
}, 2 * 1000)
As you can see, you can create an adaptor and custom what you want. For example here, you keep the cache for only 2 seconds. So the first request to /users is a real one. The second and thirst request are cache, and the last one is after the two seconds, so it's a real one.
Can you be more specific about the context of how you will keep the cache. When do you need to reload the cache, after how many times, after which event, ...?
The strategy I use is to store the value on the Vuex state.
I write all my request in Vuex actions. In every action, I check if the data already exists on the Vuex state. If it does, I simply bypass the request and return the data from the state (saving requests from being called multiple times). If it doesn't exist, I'll make the request, then store the result on the Vuex state, and return the value.
Vuex Action:
getLists({ state, commit }) {
return new Promise((resolve, reject) => {
if (state.isSetLists === false) {
getListsFromServer((error, data) => {
if (error) {
reject(error);
} else {
console.log('call to getLists successful:', data);
commit('setLists', data.lists);
resolve(data.lists);
}
});
} else {
resolve(state.lists);
}
});
},
Then, the setLists mutation handles it like so:
setLists(state, lists) {
state.isSetLists = true;
state.lists = lists;
},
This way, the user can page around all they want, and only ever call each request once.