Related
I am implementing authentication in my NextJS app using next-iron-session, currently using getServerSideProps method for that an it is working fine but I have to implement this in every page where I want to authenticate the user. I just want to implement it in HOC format or wrapper format so I don't have to rewrite this in every file. I am using the following code for that
import { withIronSession } from "next-iron-session";
const user_home = (props) => {
if (!user.isAuth) {
router.push("/");
}
// ...some other layout stuff
};
export const getServerSideProps = withIronSession(
async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
return {
props: { isAuth: false },
};
}
return {
props: { isAuth: true, user: user },
};
},
{
cookieName: "NEXT_EXAMPLE",
cookieOptions: {
secure: true,
},
password: process.env.APPLICATION_SECRET,
}
);
export default user_home;
This post is a bit old but here is my solution, with iron-session since next-iron-session has been deprecated.
Create a HOC like this
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "./session";
const WithAuth = (gssp) =>
withIronSessionSsr(async function (context) {
const user = context.req.session.user;
// you can check the user in your DB here
if (!user) {
return {
redirect: {
permanent: false,
destination: "/login",
},
}
}
return await gssp(context);
}, sessionOptions);
export default WithAuth;
Then in your page
export const getServerSideProps = WithAuth(async function (context) {
...
return {
props: { user: context.req.session.user, ... },
};
});
I think you can redirect from the server.
import { withIronSession } from "next-iron-session";
const user_home = (props) => {
// ...some other layout stuff
};
export const getServerSideProps = withIronSession(
async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
redirect: {
permanent: false,
destination: "/login",
},
}
return {
props: { user: user },
};
},
{
cookieName: "NEXT_EXAMPLE",
cookieOptions: {
secure: true,
},
password: process.env.APPLICATION_SECRET,
}
);
export default user_home;
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);
}
After a new project is created, I'd like to route the user to another page so they can add more information to the project.
This is working:
createProject() {
ProjectService.createProject(this.project)
.then(response => {
this.$router.push({
name: "project-update",
params: { id: response.data.data.id }
});
})
}
I'd like to use vuex to handle all this, but this is not working.
createProject() {
this.$store
.dispatch("project/postProject", this.project)
.then(response => {
this.$router.push({
name: "project-update",
params: { id: response.data.data.id }
});
})
.catch(() => {});
}
The error I'm getting is: "state.projects.push is not a function"
This is my postProject action in Vuex:
postProject({ commit, dispatch }, project) {
return ProjectService.createProject(project)
.then(() => {
commit('ADD_PROJECT', project);
const notification = {
type: 'success',
message: 'Your project has been created!'
};
dispatch('notification/add', notification, { root: true });
})
.catch(error => {
const notification = {
type: 'error',
message: 'There was a problem creating your project: ' + error.message
};
dispatch('notification/add', notification, { root: true });
throw error;
});
}
Looks like the context of 'this' is not reaching the router or the push function therein. How can I access the router and route to that next page?
What you can do is import your router module into your vuex module like so:
import {router} from "../main.js"
// or
import router from '../router'
export default {
actions: {
createProject () {
this.$store
.dispatch("project/postProject", this.project)
.then(response => {
router.push({
name: "project-update",
params: { id: response.data.data.id }
})
})
.catch(() => { })
}
}
}
I have a same issue but I solved by doing this:
this.$router.replace("/");
Having issue in vuex and nuxt store by using this : this.$router.push("/");
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'
});