Related
So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.
I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.
I have JWT for authentication, using the JWTSessions gem for Rails.
On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:
export default function ({ $auth, route, redirect }) {
const role = $auth.user && $auth.user.role
if (route.meta[0].requiredRole !== role) {
redirect('/login')
}
}
The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.
However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.
I have the following relevant files.
nuxt.config.js
...
plugins: [
// ...
{ src: '~/plugins/axios' },
// ...
],
// ...
modules: [
'cookie-universal-nuxt',
'#nuxtjs/axios',
'#nuxtjs/auth'
],
// ...
axios: {
baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com/v1' : 'http://localhost:3000/v1',
credentials: true
},
auth: {
strategies: {
jwtSessions: {
_scheme: '~/plugins/auth-jwt-scheme.js',
endpoints: {
login: { url: '/signin', method: 'post', propertyName: 'csrf' },
logout: { url: '/signin', method: 'delete' },
user: { url: '/users/active', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: false
}
},
cookie: {
options: {
maxAge: 64800,
secure: process.env.NODE_ENV === 'production'
}
}
},
auth-jwt-scheme.js
const tokenOptions = {
tokenRequired: true,
tokenType: false,
globalToken: true,
tokenName: 'X-CSRF-TOKEN'
}
export default class LocalScheme {
constructor (auth, options) {
this.$auth = auth
this.name = options._name
this.options = Object.assign({}, tokenOptions, options)
}
_setToken (token) {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
}
}
_clearToken () {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
this.$auth.ctx.app.$axios.setHeader('Authorization', false)
}
}
mounted () {
if (this.options.tokenRequired) {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
}
return this.$auth.fetchUserOnce()
}
async login (endpoint) {
if (!this.options.endpoints.login) {
return
}
await this._logoutLocally()
const result = await this.$auth.request(
endpoint,
this.options.endpoints.login
)
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + result
: result
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async setUserToken (tokenValue) {
await this._logoutLocally()
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + tokenValue
: tokenValue
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async fetchUser (endpoint) {
if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
return
}
if (!this.options.endpoints.user) {
this.$auth.setUser({})
return
}
const user = await this.$auth.requestWith(
this.name,
endpoint,
this.options.endpoints.user
)
this.$auth.setUser(user)
}
async logout (endpoint) {
if (this.options.endpoints.logout) {
await this.$auth
.requestWith(this.name, endpoint, this.options.endpoints.logout)
.catch(() => {})
}
return this._logoutLocally()
}
async _logoutLocally () {
if (this.options.tokenRequired) {
this._clearToken()
}
return await this.$auth.reset()
}
}
axios.js
export default function (context) {
const { app, $axios, redirect } = context
$axios.onResponseError(async (error) => {
const response = error.response
const originalRequest = response.config
const access = app.$cookies.get('jwt_access')
const csrf = originalRequest.headers['X-CSRF-TOKEN']
const credentialed = (process.client && csrf) || (process.server && access)
if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
if (process.server) {
$axios.setHeader('X-CSRF-TOKEN', csrf)
$axios.setHeader('Authorization', access)
}
const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })
if (newToken.data.csrf) {
$axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
$axios.setHeader('Authorization', newToken.data.access)
if (app.$auth) {
app.$auth.setToken('jwt_access', newToken.data.csrf)
app.$auth.syncToken('jwt_access')
}
originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
originalRequest.headers.Authorization = newToken.data.access
if (process.server) {
app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
}
return $axios(originalRequest)
} else {
if (app.$auth) {
app.$auth.logout()
}
redirect(301, '/login')
}
} else {
return Promise.reject(error)
}
})
}
This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.
In order for You not to lose Your authentication session in the system, You first need to save your JWT token to some storage on the client: localStorage or sessionStorage or as well as token data can be saved in cookies.
For to work of the application will be optimally, You also need to save the token in the store of Nuxt. (Vuex)
If You save Your token only in srore of Nuxt and use only state, then every time You refresh the page, Your token will be reset to zero, since the state will not have time to initialize. Therefore, you are redirected to the page /login.
To prevent this from happening, after you save Your token to some storage, You need to read it and reinitialize it in the special method nuxtServerInit(), in the universal mode his will be work on the server side the very first. (Nuxt2)
Then, accordingly, You use Your token when sending requests to the api server, adding to each request that requires authorization, a header of the Authorization type.
Since Your question is specific to the Nuxt2 version, for this version a working code example using cookies to store the token would be:
/store/auth.js
import jwtDecode from 'jwt-decode'
export const state = () => ({
token: null
})
export const getters = {
isAuthenticated: state => Boolean(state.token),
token: state => state.token
}
export const mutations = {
SET_TOKEN (state, token) {
state.token = token
}
}
export const actions = {
autoLogin ({ dispatch }) {
const token = this.$cookies.get('jwt-token')
if (isJWTValid(token)) {
dispatch('setToken', token)
} else {
dispatch('logout')
}
},
async login ({ commit, dispatch }, formData) {
const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
dispatch('setToken', token)
},
logout ({ commit }) {
this.$axios.setToken(false)
commit('SET_TOKEN', null)
this.$cookies.remove('jwt-token')
},
setToken ({ commit }, token) {
this.$axios.setToken(token, 'Bearer')
commit('SET_TOKEN', token)
this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
// <-- above use, for example, moment or add function that will computed date
}
}
/**
* Check valid JWT token.
*
* #param token
* #returns {boolean}
*/
function isJWTValid (token) {
if (!token) {
return false
}
const jwtData = jwtDecode(token) || {}
const expires = jwtData.exp || 0
return new Date().getTime() / 1000 < expires
}
/store/index.js
export const state = () => ({
// ... Your state here
})
export const getters = {
// ... Your getters here
}
export const mutations = {
// ... Your mutations here
}
export const actions = {
nuxtServerInit ({ dispatch }) { // <-- init auth
dispatch('auth/autoLogin')
}
}
/middleware/isGuest.js
export default function ({ store, redirect }) {
if (store.getters['auth/isAuthenticated']) {
redirect('/admin')
}
}
/middleware/auth.js
export default function ({ store, redirect }) {
if (!store.getters['auth/isAuthenticated']) {
redirect('/login')
}
}
/pages/login.vue
<template>
<div>
<!-- Your template here-->
</div>
</template>
<script>
export default {
name: 'Login',
layout: 'empty',
middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
data () {
return {
controls: {
login: '',
password: ''
},
rules: {
login: [
{ required: true, message: 'login is required', trigger: 'blur' }
],
password: [
{ required: true, message: 'password is required', trigger: 'blur' },
{ min: 6, message: 'minimum 6 length', trigger: 'blur' }
]
}
}
},
head: {
title: 'Login'
},
methods: {
onSubmit () {
this.$refs.form.validate(async (valid) => { // <-- Your validate
if (valid) {
// here for example: on loader
try {
await this.$store.dispatch('auth/login', {
login: this.controls.login,
password: this.controls.password
})
await this.$router.push('/admin')
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
} finally {
// here for example: off loader
}
}
})
}
}
}
</script>
! - You must have the following packages installed:
cookie-universal-nuxt
jsonwebtoken
jwt-decode
I think you will find my answer helpful. If something is not clear, ask!
Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.
The problem is, this creates a closure and returns old data to the interceptor when it's being called.
I am using JWT authentication using React/Node and I'm storing access tokens using Context API.
This is how my Interceptor component looks like right now:
import React, { useEffect, useContext } from 'react';
import { Context } from '../../components/Store/Store';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
const ax = axios.create();
const Interceptor = ({ children }) => {
const [store, dispatch] = useContext(Context);
const history = useHistory();
const getRefreshToken = async () => {
try {
if (!store.user.token) {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
const { data } = await axios.post('/api/auth/refresh_token', {
headers: {
credentials: 'include',
},
});
if (data.user) {
dispatch({
type: 'setStore',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
authenticated: true,
token: data.accessToken,
id: data.user.id,
name: data.user.name,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: {
items: [],
count: data.user.messages,
},
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved.reduce(function (object, item) {
object[item] = true;
return object;
}, {}),
cart: {
items: data.user.cart.reduce(function (object, item) {
object[item.artwork] = true;
return object;
}, {}),
count: Object.keys(data.user.cart).length,
},
});
} else {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
}
} catch (err) {
dispatch({
type: 'setMain',
loading: false,
error: true,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
};
const interceptTraffic = () => {
ax.interceptors.request.use(
(request) => {
request.headers.Authorization = store.user.token
? `Bearer ${store.user.token}`
: '';
return request;
},
(error) => {
return Promise.reject(error);
}
);
ax.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
console.log(error);
if (error.response.status !== 401) {
return new Promise((resolve, reject) => {
reject(error);
});
}
if (
error.config.url === '/api/auth/refresh_token' ||
error.response.message === 'Forbidden'
) {
const { data } = await ax.post('/api/auth/logout', {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'resetUser',
});
history.push('/login');
return new Promise((resolve, reject) => {
reject(error);
});
}
const { data } = await axios.post(`/api/auth/refresh_token`, {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'updateUser',
token: data.accessToken,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: { items: [], count: data.user.messages },
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved,
cart: { items: {}, count: data.user.cart },
});
const config = error.config;
config.headers['Authorization'] = `Bearer ${data.accessToken}`;
return new Promise((resolve, reject) => {
axios
.request(config)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
);
};
useEffect(() => {
getRefreshToken();
if (!store.main.loading) interceptTraffic();
}, []);
return store.main.loading ? 'Loading...' : children;
}
export { ax };
export default Interceptor;
The getRefreshToken function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.
The interceptTraffic function is where the issue persists.
It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.
You will notice that I am exporting ax (an instance of Axios where I added interceptors) but when it's being called outside this component, it references old store data due to closure.
This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.
Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.
Any help is appreciated, thanks.
Common Approach (localStorage)
It is a common practice to store the JWT in the localStorage with
localStorage.setItem('token', 'your_jwt_eykdfjkdf...');
on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage
custom-axios.js
import axios from 'axios';
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = localStorage.getItem('token');
return config;
});
export default axiosInstance;
And then, just import the Axios instance we just created and make requests.
import axios from './custom-axios';
axios.get('/url');
axios.post('/url', { message: 'hello' });
Another approach (when you've token stored in the state)
If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:
custom-axios.js
import axios from 'axios';
const customAxios = (token) => {
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = token;
return config;
});
return axiosInstance;
};
export default customAxios;
And then import the function we just created, grab the token from state, and make requests:
import axios from './custom-axios';
// logic to get token from state (it may vary from your approach but the idea is same)
const token = useSelector(token => token);
axios(token).get('/url');
axios(token).post('/url', { message: 'hello' });
I have a template that works in a system with millions of access every day.
This solved my problems with refresh token and reattemp the request without crashing
First I have a "api.js" with axios, configurations, addresses, headers.
In this file there are two methods, one with auth and another without.
In this same file I configured my interceptor:
import axios from "axios";
import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService";
export const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-Type": "application/json",
},
});
export const apiSecure = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
Authorization: "Bearer " + localStorage.getItem("Token"),
"Content-Type": "application/json",
},
export default api;
apiSecure.interceptors.response.use(
function (response) {
return response;
},
function (error) {
const access_token = localStorage.getItem("Token");
if (error.response.status === 401 && access_token) {
return ResetTokenAndReattemptRequest(error);
} else {
console.error(error);
}
return Promise.reject(error);
}
);
Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:
import api from "../api";
import axios from "axios";
let isAlreadyFetchingAccessToken = false;
let subscribers = [];
export async function ResetTokenAndReattemptRequest(error) {
try {
const { response: errorResponse } = error;
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber((access_token) => {
errorResponse.config.headers.Authorization = "Bearer " + access_token;
resolve(axios(errorResponse.config));
});
});
if (!isAlreadyFetchingAccessToken) {
isAlreadyFetchingAccessToken = true;
await api
.post("/Auth/refresh", {
Token: localStorage.getItem("RefreshToken"),
LoginProvider: "Web",
})
.then(function (response) {
localStorage.setItem("Token", response.data.accessToken);
localStorage.setItem("RefreshToken", response.data.refreshToken);
localStorage.setItem("ExpiresAt", response.data.expiresAt);
})
.catch(function (error) {
return Promise.reject(error);
});
isAlreadyFetchingAccessToken = false;
onAccessTokenFetched(localStorage.getItem("Token"));
}
return retryOriginalRequest;
} catch (err) {
return Promise.reject(err);
}
}
function onAccessTokenFetched(access_token) {
subscribers.forEach((callback) => callback(access_token));
subscribers = [];
}
function addSubscriber(callback) {
subscribers.push(callback);
}
I'm trying to integrate Auth0 into React like it is shown here.
This is how my Auth class looks like:
class Auth {
constructor() {
if (typeof window === 'undefined') return;
this.lock0 = new Auth0Lock(clientID, domain, {
container: 'login-container',
theme: {
primaryColor: '#FFC107',
},
languageDictionary: {
emailInputPlaceholder: 'your_email#here.com',
title: 'Authorize',
},
auth: {
audience,
redirectUrl: redirectUri,
responseType,
params: { scope },
},
});
this.auth0 = new auth0.WebAuth({
domain,
clientID,
redirectUri,
audience,
responseType,
scope,
});
this.show = this.show.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.handleAuthentication = this.handleAuthentication.bind(this);
this.isAuthenticated = this.isAuthenticated.bind(this);
console.log('Auth class initialized');
}
show() {
this.lock0.show();
}
login() {
this.auth0.authorize();
}
logout(history) {
// Clear Access Token and ID Token from local storage
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
// navigate to the home route
history.replace(HOME_ROUTE);
}
handleAuthentication(history) {
this.auth0.parseHash((err, authResult) => {
console.log('#handleAuthentication', { err, authResult });
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult, history);
history.replace(HOME_ROUTE);
} else if (err) {
history.replace(HOME_ROUTE);
console.error(err);
}
});
}
setSession(authResult, history) {
// Set the time that the Access Token will expire at
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
localStorage.setItem('expires_at', expiresAt);
// navigate to the home route
history.replace(HOME_ROUTE);
}
isAuthenticated() {
// Check whether the current time is past the
// Access Token's expiry time
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
}
const auth = new Auth();
export default auth;
What I'm getting in the console is like 20 errors looking like this:
Failed to load resource: the server responded with a status of 400 ()
As soon as I comment out the this.lock0 = Auth0Lock... part - they are gone.
What am I doing wrong?
Im building my app with VueJS and Vuex and I'm facing the issue when I have Multiple modules using the same data fields. Its about API configuration like dat.
getUsers ({ state, commit }) {
axios.get(urls.API_USER_URL).then( response => {
let data = response.data;
parseApi(state, data, 'user');
}).catch( err => {
console.log('getUser error: ', err);
})
},
And another function in other Modules is like
getPosts ({ state, commit }) {
axios.get(urls.API_POST_URL).then( response => {
let data = response.data;
parseApi(state, data, 'posts');
}).catch( err => {
console.log('getUser error: ', err);
})
},
I would like to know if I can just inheritence my Module and add additional datafields / functions in there?
My every module would have message and status field which I getting in response of my API.
export default {
state : {
message : "",
status : 0
},
parseApi: function(state, data, property) {
if (data.hasOwnProperty('message')) {
state.message = data.message;
}
if (data.hasOwnProperty('status')) {
state.status = data.status;
}
if (data.hasOwnProperty(property)) {
state[property] = data[property];
}
}
}
It would be something like that.
Is there a way to write this code once and have it in every module Im using?
EDITED:
I even cant get this apiParse function in there, I need to make muttation for those fields. But repeting it all time is pointless... Any advices?
I put my reusable vuex code in small classes. E.g.
crud.js
export default class {
constructor ( endpoint ) {
this.state = {
endpoint: endpoint,
meta: {},
status: null,
known: [],
currentId: null,
};
this.getters = {
id: state => id => state.known.find( o => o.id === id )
};
this.actions = {
async store( context, payload ) {
*(call to API)*
},
async update( context, payload ) {
*(call to API)*
},
*...etc*
};
this.mutations = {
STORED(state, item) {
state.known.push(item);
},
*...etc*
};
}
}
Then I can use it in all of my modules:
user.module.js
import Crud from '/crud';
var crud = new Crud('/api/users');
const state = {
...crud.state,
};
const getters = {
...crud.getters,
};
const actions = {
...crud.actions,
};
const mutations = {
...crud.mutations,
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
Developing a little bit more Erin's response, you can define a base class with common features like this:
export default class BaseModule {
protected state() {
return {
isLoading: false,
};
};
protected getters() {
return {
isLoading(s) {
return s.isLoading;
},
};
};
protected actions() {
return {};
};
protected mutations() {
return {
[START_TRANSACTION]: (s) => {
s.isLoading = true;
},
[END_TRANSACTION]: (s) => {
s.isLoading = false;
},
};
}
protected modules() {
return {};
};
public getModule = () => {
return {
namespaced: true,
state: this.state(),
getters: this.getters(),
actions: this.actions(),
mutations: this.mutations(),
modules: this.modules(),
};
}
}
You can now extend/override only the parts you need in derived classes, with class inheritance; for example, if you need to extend the modules...:
import BaseModule from './BaseModule';
import rowDensity from '#/store/modules/reusable/rowDensity';
export default class ItemListModule extends BaseModule {
protected modules() {
return {
...super.modules(),
rowDensity,
};
};
}
Finally, to use them as modules in the store, you can instantiate them and call .getModule():
import Vue from 'vue';
import Vuex from 'vuex';
import ItemListModule from './modules/ItemListModule';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export const MODULE_NAMESPACES = {
List: 'list',
};
export default new Vuex.Store({
modules: {
[MODULE_NAMESPACES.List]: new ItemListModule().getModule(),
},
strict: debug,
});
I figured out some inheritance with the state fields according to:
https://vuex.vuejs.org/en/modules.html#namespacing
export default {
namespaced: true,
state,
getters,
actions,
mutations,
modules : {
apiResponses
}
}
I exported apiResponses module after the module user with namespaced and next i did the same thing with posts.
The namespaces inherited those message / status states and their mutations and which i just called in my user and post module. Now they are working corectly.
My message muttation form apiResponses:
[types.SET_MESSAGE] (state, message) {
state.message = message;
},
Works inside actions of my user modules
if (data.hasOwnProperty('message')) {
commit(types.SET_MESSAGE, data.message);
}
Then in my commponent I just call.
computed: {
...mapGetters({
user : 'user/user',
userMessage : 'user/message',
post: 'post/monitoring',
postMessage : 'post/message',
}),
},
EDITED
The last part of my issue is like that.
I got action inside apiResponse Module
let actions = {
getResponseParsed({commit}, payload) {
console.log(payload)
if (payload.data.hasOwnProperty('message')) {
commit(types.SET_MESSAGE, payload.data.message);
}
if (payload.data.hasOwnProperty('status')) {
commit(types.SET_STATUS, payload.data.status);
}
if (payload.data.hasOwnProperty(payload.property)) {
commit(payload.mutation, payload.data[payload.property]);
}
}
}
And then inside my user and other module i called it like:
getUser ({ state, commit, dispatch }) {
axios.get(urls.API_GET_USER_URL).then( response => {
let data = response.data;
dispatch('getResponseParsed', {
data : data,
mutation : types.SET_USER,
property : 'user'
});
});
},
And the last thing, we need to make this new module reusable to according to docs we need to create it like a components.
export default {
state() {
return {
message : '',
status : 0,
}
},
getters,
mutations,
actions
}
With the state as function :)
Hope somone else got same issue :D
here is what I've done:
first of all, I created a mainApi.js whose duty is to just make connection with apis
mainApi.js
import axios from "#/plugins/axios";
export default {
get(url ,id){
return axios.get(`/${url}/${id}`);
},
getAll(url, filter) {
return axios.get(`/${url}`, {params: {...filter}});
},
create(url ,teBeCreated){
return axios.post(`/${url}`, teBeCreated);
},
update(url ,toBeUpdated){
return axios.put(`/${url}/${toBeUpdated.oid}`, toBeUpdated);
},
delete(url ,id){
return axios.delete(`/${url}/${id}`);
},
}
second: I wrote a base class to define needed functions to store data. then this class can be inherited by other store modules.
gate.js
import mainApi from '#/api/main'
import store from '#/store'
export default class {
constructor() {
this.state = {
view: null,
list: [],
};
this.getters = {
view: (state) => state.view,
list: (state) => state.list,
}
this.mutations = {
SET_VIEW(state, payload) {
state.view = payload;
},
SET_LIST(state, payload) {
state.list = payload;
},
UN_SET_VIEW(state) {
state.view = null;
},
UN_SET_LIST(state) {
state.list = [];
},
}
this.actions = {
get({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
resolve(response)
})
.catch(error => {
console.log("error in get method in gate store: ", error);
commit('UN_SET_VIEW');
reject(error)
})
});
},
getAll({ commit }, { url, filter }) {
return new Promise((resolve, reject) => {
mainApi.getAll(url, filter)
.then(response => {
commit('SET_LIST', response.data.data);
resolve(response)
})
.catch(error => {
console.log("error in getAll method in gate store: ", error);
commit('UN_SET_LIST');
reject(error)
})
});
},
create({ commit }, { url, params }) {
return new Promise((resolve, reject) => {
mainApi.create(url, params)
.then(response => {
resolve(response)
})
.catch(error => {
console.log("error in create method in gate store: ", error);
reject(error)
});
});
},
update({ commit }, { url, params }) {
return new Promise((resolve, reject) => {
mainApi.update(url, params)
.then(response => {
resolve(response)
})
.catch(error => {
console.log("error in update method in gate store: ", error);
reject(error)
})
})
},
delete({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.delete(url, id)
.then(response => {
resolve(response);
})
.catch(error => {
console.log("error in delete method in gate store: ", error);
reject(error)
})
});
},
}
}
third: now, we can define as many separate store modules as we need. as you can see below, in each module we just need to get the data retrieved from views and pass them to mainApi (gate.js base class's functions and methods are all part of our modules) and manipulate with received data.
someStore.js
import Gate from '#/store/modules/gate'
let gate = new Gate();
const url = 'customUrl'
const gateStates = { ...gate.state }
const gateGetters = { ...gate.getters }
const gateMutations = { ...gate.mutations }
const state = {
...gateStates,
};
const getters = {
...gateGetters,
};
const mutations = {
...gateMutations,
};
const actions = {
get: ({ commit }, id) => gate.actions.get({ commit }, { url, id }),
getAll: ({ commit }) => gate.actions.getAll({ commit }, {url, filter: {}}),
create: ({ commit }, params) => gate.actions.create({ commit }, { url, params }),
update: ({ commit }, params) => gate.actions.update({ commit }, { url, params }),
delete: ({ commit }, id) => gate.actions.delete({ commit }, { url, id })
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
finally we should import our modules and define them as "vuex store modules" so:
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import someModule from './modules/someModule'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
someModule
},
plugins: {}
})
in this example I used anotherPromise because I needed the server responses directly in my views. if you want to just use responses in your store, there is no need for these Promises and they should be removed as below:
in gate.js
change this
get({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
resolve(response)
})
.catch(error => {
commit('UN_SET_VIEW');
console.log("error in getOne method in gate store: ", error);
reject(error)
})
});
},
to this
get({ commit }, { url, id }) {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
})
.catch(error => {
commit('UN_SET_VIEW');
console.log("error in getOne method in gate store: ", error);
})
},
in this way, you have list and view parameters in each module and they can be easily called in your views:
someView.vue
created() {
store.dispatch('someModule/get', this.$route.params.id)
}
computed: {
view() {
return store.getters('someModule/view')
}
}
As a personal challenge I wanted to be able to create a pure ES6 class that could express this need (meaning no annotation allowed). I thus created an AbstractModule class defining the high level operations:
export default class AbstractModule {
constructor(namespaced = true) {
this.namespaced = namespaced;
}
_state () {
return {}
}
_mutations () {
return {}
}
_actions () {
return {}
}
_getters () {
return {}
}
static _exportMethodList (instance, methods) {
let result = {};
// Process methods when specified as array
if (Array.isArray(methods)) {
for (let method of methods) {
if (typeof method === 'string') {
result[method] = instance[method].bind(instance);
}
if (typeof method === 'function') {
result[method.name] = method.bind(instance);
}
// else ignore
}
}
// Process methods when specified as plain object
if (typeof methods === "object") {
for (const [name, method] of Object.entries(methods)) {
if (typeof method === 'string') {
result[name] = instance[method].bind(instance);
}
if (typeof method === 'function') {
result[name] = method.bind(instance);
}
}
}
// Process methods when specified as single string
if (typeof methods === 'string') {
result[name] = instance[methods].bind(instance);
}
// Process methods when specified as single callback
if (typeof methods === 'function') {
result[name] = methods.bind(instance);
}
return result;
}
static module() {
let instance = new this();
console.log(instance);
return {
namespaced: instance.namespaced,
state: instance._state(),
mutations: AbstractModule._exportMethodList(instance, instance._mutations()),
actions: AbstractModule._exportMethodList(instance, instance._actions()),
getters: AbstractModule._exportMethodList(instance, instance._getters())
}
}
}
From this I created my own class module by redefining the parent methods I wanted to customize this way:
export default class QuestionModule extends AbstractModule{
constructor(question) {
super();
this.question = question;
}
selectLine (state, line) {
this.question.selectLine(line);
}
unselectLine (state, line) {
this.question.unselectLine(line);
}
submit ({ state, commit, rootState }) {
/** API call */
}
_state () {
return this.question;
}
_mutations () {
return [this.selectLine, this.unselectLine, this.validate];
}
_actions () {
return this.submit;
}
}
Final step is to declare my class module into the Vuex store (through a call to the module static method):
const store = new Vuex.Store({
modules: {
question: QuestionModule.module()
},
strict: process.env.NODE_ENV !== 'production'
});