hope you’re all well and safe!
I'm currently working on a Vue 3 application with Pinia as my store, Auth0 Vue SDK for authentication/authorization and Axios to call my backend API.
In Auth0 docs, they recommend an access token be retrieved using the getAccessTokenSilently() method everytime I want to call my backend API:
const { getAccessTokenSilently } = useAuth0();
const accessToken = await getAccessTokenSilently();
The problem is I have to type this whenever I use axios in my component files to call my backend API with the access token.
Since I have too many endpoints, my plan is to pass the access token once in an axios request interceptor and use Pinia actions to call my APIs.
I’ve created a /config/axios.js file in my application that contains the following:
//Import Axios Library and Auth0
import axios from 'axios';
import { useAuth0 } from "#auth0/auth0-vue"
//Create instance of axios
const instance = axios.create({
baseURL: 'http://localhost:5000/api/v1'
});
// Create a request interceptor for my instance and get accessToken on the fly
instance.interceptors.request.use(async (config) => {
const { getAccessTokenSilently } = useAuth0();
const accessToken = await getAccessTokenSilently();
config.headers['Authorization'] = accessToken;
return config;
}, (error) => {
return Promise.reject(error)
});
export default instance;
Simple enough, just create a baseURL and intercept requests to add the authorization header with the access token.
Now in Pinia, I've created a user store that'll fetch users with the axios config as seen below:
//Import the Pinia Library
import { defineStore } from 'pinia'
//Import the Axios Library for API calls
import axiosConfig from "#/config/axios.js"
export const useUserStore = defineStore('user', {
state: () => ({
user:{}
}),
getters: {
getUser(state){
return state.user
}
},
actions: {
async fetchUser(){
try {
const data = await axiosConfig.get('/profile')
this.user = data.data
} catch (error) {
console.log("User Pinia error: " + error)
}
}
},
})
And lastly in my component file, I just import the store and call the Pinia action fetchUsers.
When trying an axios call, I get the following error!
TypeError: auth0_auth0_vue__WEBPACK_IMPORTED_MODULE_5_.useAuth0() is undefined
I can't figure out how to retrieve the access token from auth0 library in my interceptor function.
A similar question was raised as an issue on auth0-vue github:
https://github.com/auth0/auth0-vue/issues/99
The above link describes numerous approaches. I went for the plugin approach that is described in this PR, namely:
https://github.com/auth0-samples/auth0-vue-samples/commit/997f262dabbab355291e5710c51d8056a5b142cf
But the issue was officially resolved by offering a mechanism to allow the sdk from outside a vue component:
https://github.com/auth0/auth0-vue/issues/99#issuecomment-1099704276
Related
I am sorry if it is obvious/well-covered elsewhere, but my google-fu has been failing me for over a full day by now. What I would like to achieve is a rich component-level handling of request errors: toaster notifications, status bars, you name it. The most obvious use case is auth guards/redirects, but there may be other scenarios as well (e.g. handling 500 status codes). For now, app-wide interceptors would do, but there is an obvious (to me, at least) benefit in being able to supplement or override higher-level interceptors. For example, if I have interceptors for 403 and 500 codes app-wide, I might want to override an interceptor for 403, but leave an interceptor for 500 intact on a component level.
This would require access to component properties: I could then pass status messages in child components, create toaster notifications with custom timeouts/animations and so on. Naively, for my current app-wide problem, this functionality belongs in App.vue, but I can not figure out how to get access to App in axios.interceptors.response using the current plugin arrangement and whether it is okay to use a single axios instance app-wide in the first place.
The trimmed down code I have tried so far (and which seems the most ubiquitous implementation found online) can be found below. It works with redirects, producing Error: getTranslators: detection is already running in the process (maybe because another 401 happens right after redirect with my current testing setup). However, import Vue, both with curly brackets and without, fails miserably, and, more importantly, I have no way of accessing app properties and child components from the plugin.
// ./plugins/axios.js
import axios from 'axios';
import { globalStorage } from '#/store.js';
import router from '../router';
// Uncommenting this import gives Uncaught SyntaxError: ambiguous indirect export: default.
// Circular dependency?..
// import Vue from 'vue';
const api = axios.create({
baseURL: import.meta.env.VUE_APP_API_URL,
});
api.interceptors.response.use(response => response,
error => {
if (error.response.status === 401) {
//Vue.$toast("Your session has expired. You will be redirected shortly");
delete globalStorage.userInfo;
localStorage.setItem('after_login', router.currentRoute.value.name);
localStorage.removeItem('user_info');
router.push({ name: 'login' });
}
return Promise.reject(error);
});
export default api;
// --------------------------------------------------------------------------
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import axios from './plugins/axios'
import VueAxios from 'vue-axios'
const app = createApp(App)
app.use(router)
.use(VueAxios, axios)
.mount('#app')
So, then, how do I get access to component properties in interceptors? If I need them to behave differently for different components, would I then need multiple axios instances (assuming the behavior is not achieved by pure composition)? If so, where to put the relevant interceptor configuration and how to ensure some parts of global configuration such as baseURL apply to all of these instances?
I would prefer not having more major external dependencies such as Vuex as a complete replacement for the existing solution, but this is not a hill to die on, of course.
Instead of using axios's interceptors, you should probably create a composable. Consider the following:
composables/useApiRequest.js
import axios from 'axios';
import { useToast } from "vue-toastification";
const useApiRequest = () => {
const toast = useToast();
const fetch = async (url) => {
try {
await axios.get(url);
} catch (error) {
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
}
};
return {
fetch,
};
};
export default useApiRequest;
Here we're creating a composable called useApiRequest that serves as our layer for the axios package where we can construct our api requests and create generic behaviors for certain response attributes. Take note that we can safely use Vue's Composition API functions and also components such as the vue-toastification directly in this composable:
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
We can import this composable in the component and use the fetch function to send a GET request to whatever url that we supply:
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
const searchBar = ref('');
const request = useApiRequest();
const retrieveResult = async () => {
await request.fetch(`https://api.ebird.org/v2/data/obs/${searchBar.value}/recent`);
}
</script>
And that's it! You can check the example here.
Now, you mentioned that you want to access component properties. You can accomplish this by letting your composable accept arguments containing the component properties:
// `props` is our component props
const useApiRequest = (props) => {
// add whatever logic you plan to implement for the props
}
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
import { DEFAULT_STATUS } from '../constants';
const status = ref(DEFAULT_STATUS);
const request = useApiRequest({ status });
</script>
Just try to experiment and think of ways to make the composable more reusable for other components.
Note
I've updated the answer to change "hook" to "composable" as this is the correct term.
I want to trigger a Google Composer airflow dag using Appscript. Is there any way to do it via rest API or another way.
If it is possible please suggest the solution.
Airflow has an endpoint that allows to trigger a DAG through its REST API, however it’s not possible to access it directly, since within the Cloud Composer architecture, the Airflow web server is located under an App Engine flexible environment. By default, the Airflow web server is integrated with Identity-Aware Proxy (IAP) and authentication is required.
Based on that, I found an example in the Cloud Composer documentation, that guides you to trigger a DAG using Cloud Functions, although the code is in JavaScript I don’t think it’s possible to execute it by Google App Script.
On the other hand, a workaround is to follow the Triggering DAGs guide changing some settings as follows.
In the creation of the function instead of setting the trigger type as Cloud Storage set it as HTTP, and check the “Allow unauthenticated invocations” for test purpose. An URL will be displayed, the goal is that every time that URL is accessed the DAG is executed.
Modify the first part of the index.js file, since no data would be passed as parameters and also the makeIapPostRequest function to return the response of the API call.
exports.triggerDag = async (req, res) => { // Modification
// Fill in your Composer environment information here.
// The project that holds your function
const PROJECT_ID = 'your-project-id';
// Navigate to your webserver's login page and get this from the URL
const CLIENT_ID = 'your-iap-client-id';
// This should be part of your webserver's URL:
// {tenant-project-id}.appspot.com
const WEBSERVER_ID = 'your-tenant-project-id';
// The name of the DAG you wish to trigger
const DAG_NAME = 'composer_sample_trigger_response_dag';
// Other constants
const WEBSERVER_URL = `https://${WEBSERVER_ID}.appspot.com/api/experimental/dags/${DAG_NAME}/dag_runs`;
const USER_AGENT = 'gcf-event-trigger';
const BODY = {conf: ‘’}; // Modification
// Make the request
try {
const iap = await authorizeIap(CLIENT_ID, PROJECT_ID, USER_AGENT);
const apiReponse = await makeIapPostRequest(WEBSERVER_URL, BODY, iap.idToken, USER_AGENT); // Modification
res.status(200).send('DAG_running!'); // Modification
} catch (err) {
console.error('Error authorizing IAP:', err.message);
throw new Error(err);
}
};
const makeIapPostRequest = async (url, body, idToken, userAgent) => {
const res = await fetch(url, {
method: 'POST',
headers: {
'User-Agent': userAgent,
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
console.error('Error making IAP post request:', err.message);
throw new Error(err);
}
return {
apiRes: res.ok, // Modification
};
};
At this point, anything else has to be changed, so in your Script file execute the next instructions in order to trigger the DAG.
function myFunction() {
var response = UrlFetchApp.fetch("Cloud-function-URL");
Logger.log(response.getAllHeaders());
}
Finally, verify in the Airflow web interface if the DAG was triggered.
I actually using Axios in a Nuxt.js project to catch Json file to display it on my website. I would like to use a variable store in Vuex to select the path for my Axios get request.
Here's what my <script> looks like:
<script>
import axios from 'axios'
import vuex from 'vuex'
export default {
async asyncData({ }) {
const json = await axios.get(`https://myurl.com/${$store.getters["getPath"]}`)
return { json }
}
}
</script>
Pretty sure that isn't the good method to do it. I receive the console error : ReferenceError: store is not defined.
Thanks in advance!
While using the asyncData method you don't have access to this keyword so the asyncData method receives the context object as an argument from which you can get access to Axios, Vuex etc. And thus you don't need to import Axios and Vuex.
export default {
async asyncData({ $axios, store }) { //here we get Axios and Vuex
const json = await $axios.get(`https://myurl.com/${store.getters["getPath"]}`)
return { json }
}
}
I have an issue about POST data at add student page. I'm using ionic 3 and backend API restful laravel, I tried this POST in postman and it's works, but I got an error message "invalid token or token not provided" after click Add button (refer the first picture). I'm not sure how to write method with auth token.
You can refer my provider (second image) ,addstudent.ts (third image), button html (forth image) for reference
Thanks in advance.
Suppose you stored your token in a storage (e.g localStorage or sessionStorage), you can do the following:
Step1: Add Auth provider and extend BaseRequestOptions
import { BaseRequestOptions, RequestOptions, RequestOptionsArgs } from '#angular/http';
import { Injectable } from '#angular/core';
#Injectable()
export class AuthProvider extends BaseRequestOptions {
constructor() {
super();
this.headers.set('Content-Type', 'application/json');
}
merge(options?: RequestOptionsArgs): RequestOptions {
let newOptions = super.merge(options);
let accessToken = localStorage.getItem('accessToken');
if (accessToken !== null && accessToken !== undefined) {
let token: AcessTokenResponse = JSON.parse(accessToken);
newOptions.headers.set('Authorization', token.token_type + ' ' + token.access_token);
}
return newOptions;
}
}
Step2: Add AuthProvider under providers in app.module.ts file
As per my exprience you need to add RequestOptions in your POST call like below:
let options = new RequestOptions({
headers: headers
});
this.http.post(this.apiUrl+'/addstudent', addStudent, options)
As in your code you are creating Header but not passing it into post request.
Try above code Hope this will help you to get your API work.
I want to put all my functions that talk to the server and fetch data into a single reusable file in VueJS.
Plugins don't seem to be the best alternative. Template less components..?
In total there are 4 ways:
Stateless service: then you should use mixins
Stateful service: use Vuex
Export service and import from a vue code
any javascript global object
I am using axios as HTTP client for making api calls, I have created a gateways folder in my src folder and I have put files for each backend, creating axios instances, like following
myApi.js
import axios from 'axios'
export default axios.create({
baseURL: 'http://localhost:3000/api/v1',
timeout: 5000,
headers: {
'X-Auth-Token': 'f2b6637ddf355a476918940289c0be016a4fe99e3b69c83d',
'Content-Type': 'application/json'
}
})
Now in your component, You can have a function which will fetch data from the api like following:
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
As I assume you want to re-use this method in multiple components, you can use mixins of vue.js:
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
So you can add a method in mixin and it will be available in all the components, where mixin will be mixed. See following example:
// define a mixin object
var myMixin = {
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin]
})
// alternate way to have a mixin while initialising
new Vue({
mixins: [myMixin],
created: function () {
console.log('other code')
}
})
I'm using Vue Resource mostly.
1.I create new file where I do connection to API endpoint using Vue.http.xxx.So let's say we have endpoint that output the posts.Create new directory in your project, I call it services, and then create file called PostsService.js - content looks like this:
import Vue from 'vue'
export default {
get() {
return Vue.http.get('/api/posts)
}
}
Then I go to component where I want use this service, and import it
import PostsService from '../services/PostsService'
export default {
data() {
return {
items: []
}
},
created() {
this.fetchPosts()
},
methods: {
fetchPosts() {
return PostsService.get()
.then(response => {
this.items = response.data
})
}
}
}
For more info about this approach, feel free to check my repo on GitHub https://github.com/bedakb/vuewp/tree/master/public/app/themes/vuewp/app
I suggest creating an API Provider that you can access from anywhere in your app.
Simply create a src/utils folder and inside of it a file called api.js.
In it, export your wrapper that knows how to communicate with your API as an object or a ES6 static class (I prefer how the latter looks and works if you're not afraid of classes). This provider can use any HTTP request library that you like and you can easily swap it later by changing a single file (this one) instead of hunting down the whole codebase. Here's an example of using axios, assuming we have a REST API available at api.example.com/v1 that uses SSL:
import axios from 'axios'
import { isProduction, env } from '#/utils/env'
const http = null // not possible to create a private property in JavaScript, so we move it outside of the class, so that it's only accessible within this module
class APIProvider {
constructor ({ url }) {
http = axios.create({
baseURL: url,
headers: { 'Content-Type': 'application/json' }
})
}
login (token) {
http.defaults.headers.common.Authorization = `Bearer ${token}`
}
logout () {
http.defaults.headers.common.Authorization = ''
}
// REST Methods
find ({ resource, query }) {
return http.get(resource, {
params: query
})
}
get ({ resource, id, query }) {
return http.get(`${resource}/${id}`, {
params: query
})
}
create ({ resource, data, query }) {
return http.post(resource, data, {
params: query
})
}
update ({ resource, id, data, query }) {
return http.patch(`${resource}/${id}`, data, {
params: query
})
}
destroy ({ resource, id }) {
return http.delete(`${resource}/${id}`)
}
}
export default new APIProvider({
url: env('API_URL') // We assume 'https://api.example.com/v1' is set as the env variable
})
Next, in your main.js file or wherever else you bootstrap the Vue app, do the following:
import api from '#/src/utils/api'
Vue.$api = api
Object.defineProperty(Vue.prototype, '$api', {
get () {
return api
}
})
Now you can access it anywhere in your Vue app as well as anywhere you import Vue itself:
<template>
<div class="my-component">My Component</div
</template>
<script>
export default {
name: 'MyComponent',
data () {
return {
data: []
}
},
async created () {
const response = await this.$api.find({ resource: 'tasks', query: { page: 2 } })
this.data = response.data
}
}
</script>
or:
// actions.js from Vuex
import Vue from 'vue'
export async function fetchTasks ({ commit }) {
const response = await Vue.$api.find({ resource: 'tasks', query: { page: 2 } })
commit('SAVE_TASKS', response.data)
return response
}
Hope this helps.
I think for your simple question the answer could be any ES6 module containing functions (equivalent to methods in class in ANgular) and directly importing them in components using ES6 imports and exports. There are no such services that could be injected in components.
You can make your own service where you can place all your HTTP server calls and then import that to the components where you want to use them.
Best is to make use of Vuex for complex state management applications because in Vuex you are able to handle all async calls via actions which always run asynchronously and then commit the mutation once you have the result.Mutation will directly interact with the state and will update it in an immutable manner (which is preferred). This is stateful approach.
There are other approaches as well. But these are the ones which I follow in my code.