Vue router - on refreshing browser window logs out my admin user - vue.js

I have a router.beforeEach method that works perfectly when going through vue router but refreshing the page logs me back out
router.beforeEach((to, from, next) => {
if(to.matched.some(record => record.meta.isAdmin)) {
if (store.getters.isAdmin) {
next();
return;
}
next('/dashboard');
} else {
next();
}
});
So even if my admin status is "admin" I still get redirected to dashboard. Any ideas why!
EDIT:
This is how I am loggin in the user and storing their session/token
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
// set up our api end point
var get_url = window.location;
var base_url = get_url .protocol + '//' + get_url.host;
const api_base = base_url + '/api/v1';
export default new Vuex.Store({
state: {
status: false,
token: localStorage.getItem('token') || '',
user: {}
},
mutations: {
auth_request(state){
state.status = 'loading';
},
auth_success(state, {token, user}) {
state.status = 'success';
state.token = token;
state.user = user;
},
auth_error(state){
state.status = 'error';
},
logout(state){
state.status = '';
state.token = '';
},
set_user(state, user) {
state.user = user;
}
},
actions: {
login({commit}, user){
return new Promise((resolve, reject) => {
commit('auth_request');
axios({url: api_base + '/login', data: user, method: 'post' })
.then(resp => {
const token = resp.data.data;
localStorage.setItem('token', token);
axios.defaults.headers.common['Authorization'] = token;
commit('auth_success', {token, token});
resolve(resp);
})
.catch(err => {
commit('auth_error');
localStorage.removeItem('token');
reject(err);
});
});
},
},
getters : {
isLoggedIn: state => !!state.token,
authStatus: state => state.status,
getUser: state => state.user,
isAdmin: state => state.user.admin
}
});
So I am storing the token in localStorage - I am not sure if the local storage is actually working as I don't know how to see my local storage contents (though imagine this is easy to find out).

I'll make some assumptions about your app since I don't know the details, but here's my take.
When your app is initially loaded (i.e. on page refresh), the Vuex state is set to the initial values, which means the user is not logged in. The user has to go through the login process that your app provides, and then the Vuex state is updated with information about the logged in user.
If you refresh the page, your Vuex state is lost and you have to go through the login process again.
If you want to persist data across page reloads, you have to store some data on the client side (typically in LocalStorage). How this is done exactly can vary from app to app since it depends on how you have implemented logged in sessions; are you storing the session in a cookie, or are you using an auth token that you send with each request?
When the app is initially loaded, one of the first things it should do is check if there is a logged in session stored, and if so attempt to authenticate it to make sure it is still valid and retrieve from the server up-to-date information about that session (i.e. information about the logged in user). At minimum, you could store the user object in LocalStorage after logging in and then retrieve it when the app loads.
This is a big topic and I can't give you a succinct answer here, but I've given you some things to think about so you can research this topic further.

Related

Vue doesn't fetch data from API in first render

After logging in I call await router.push('/'); to redirect to the home page where I load users and I get this error GET http://localhost:8080/users 401 then when I refrehs the page in the exact same component I get the data just fine with a 200 status. I'm not sure what's going on
async login (username, password) {
const response = await axios.post('/auth/login', {
username: username,
password: password
});
this.user = response.data;
localStorage.setItem('user', JSON.stringify(this.user));
await router.push('/');
},
This is the function I call after logging in
This is the router.js
import { createRouter, createWebHistory } from 'vue-router';
import Login from '../views/Auth/Login.vue';
import { useAuthStore } from '../stores/auth.store.js';
import IndexUser from "../views/Users/IndexUser.vue";
import IndexHive from '../views/Hives/IndexHive.vue';
const routes = [
{ path: '/', name: 'Home', component: IndexUser },
{ path: '/login', name: 'Login', component: Login },
{ path: '/users', redirect: { name: 'Home' } },
{ path: '/users/create', name: 'CreateUser', component: CreateUser },
{ path: '/hives', name: 'IndexHive', component: IndexHive }
];
import CreateUser from '../views/Users/CreateUser.vue';
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach(to => {
const authStore = useAuthStore();
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
if (authRequired && !authStore.user) {
return '/login';
}
})
export default router;
This is the component I redirect to after logging in
onMounted( async () => {
const response = await axios.get('/users');
users.value = response.data;
})
Devtools
Network tab
Axios Error
details of request/response
Response of login
Update 2
Having seen the code, I think the problem is here:
import axios from "axios";
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
if (localStorage.getItem('user')) {
const user = JSON.parse(localStorage.getItem('user'));
axios.defaults.headers.common['Authorization'] = `Bearer ${user?.accessToken}`;
}
this will read the axios.defaults.headers when the helpers/axios.js file is loaded. This is why axios.get('/users'); only works on second load, or rather only when the authentication is already loaded into localStorage. A change to the user object or a local storage will not update since this code only runs once at the beginning, the change to axios.defaults.headers needs to be dynamic.
Update
if setTimeout didn't work that could be due to a different issue. Also, if your request works a second time, but it also works if the authentication is passed directly, it seems to me that it has something to do with the authentication being handled implicitly.
I think what's happening is that you are creating multiple instances of axios and relying on shared authentication
// create single axios instance
export const api = axios.create({
withCredentials: true,
baseURL: BASE_URL // optional
})
// then use
await api.post('/auth/login', {
username: username,
password: password
});
// and
await api.get('/users');
This might make the axios instance remember the authentication information between calls. It may still require handling race condition if you have an app that doesn't wait on the login request to finish.
I think this is just an issue with a race condition
POST:/login and GET:/users requests appear to be done in parallel.
onMounted( async () => {
// this should wait until the `login` has been handled
const response = await axios.get('/users');
users.value = response.data;
})
I don't see how you call login so can't offer the the exact solution, but if you can store the login request state as a reactive variable, you can do something like
watch: {
loginState:{
immediate: true
handler(value){
if (value === LOADED) {
const response = await axios.get('/users');
users.value = response.data;
}
}
}
})
here's what the changes to the authStore might look like
export const STATES = {
INIT:"INIT",
PROCESSING:"PROCESSING",
ERROR:"ERROR",
LOADED:"LOADED",
}
export const loginState = ref(STATES.INIT);
async login (username, password) {
loginState.value = STATES.PROCESSING
try{
const response = await axios.post('/auth/login', {
username: username,
password: password
});
loginState.value = STATES.LOADED
this.user = response.data;
localStorage.setItem('user', JSON.stringify(this.user));
await router.push('/');
}catch(e){
// handle error
loginState.value = STATES.ERROR
}
},

Auth token not reloaded

I have a problem with an authToken in my Vue 3 application (very beginner in JS and Vue).
When the user is authenticated (after a post /login), the app receives a token which is stored in the local storage (I know it is perhaps not a good idea, it will be another question later).
With this token, the app can make some GETs to the endpoint. All is working fine.
Of course, the user can logout. In this case I remove the token from the local storage and the user is redirected to the login page.
The problem is now : when the user reconnects again, a new token is received and stored. But all the GET requests are made with the previous token. So of course all the requests are refused (the previous token is no more on the server).
When the app receives the token :
export function loginUser(email, password) {
return new Promise(async (resolve, reject) => {
try {
let res = await axios({
url: `${REST_ENDPOINT}/login`,
method: 'POST',
data: {
email: email,
password: password
}
})
setAuthToken(res.data)
resolve()
}
catch (err) {
console.error('Caught an error during login:', err.request.response)
reject(JSON.parse(err.request.response))
}
})
}
export function logoutUser() {
clearAuthToken()
}
export function setAuthToken(token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token.token}`
console.log('je suis dans le setAUthToken')
localStorage.setItem(AUTH_TOKEN_KEY, token.token)
}
And for the requests, I created a repository system. So in my component I just have :
import { RepositoryFactory } from "../../repositories/RepositoryFactory";
const OrganizationsRepository = RepositoryFactory.get("organizations");
export default {
name: "About",
data() {
return {
isLoading: false,
organizations: [],
page: 1,
filterByName: '',
filterByStateId: 'VALIDATED',
};
},
created() {
this.page = 1
this.getOrganizations();
},
methods: {
async getOrganizations() {
this.isLoading = true;
console.log('avant le get dans la view')
const { data } = await OrganizationsRepository.getOrganizations(this.buildParams());
this.isLoading = false;
this.organizations = data;
},
I am sure (but to confirm) that it is normal because when the app is loading, the token is stored and no more changed (thanks to the import just below).
So in this case, how to force the component to reload the token for each request ?

Retrieve user settings from server

I have a Vue.js app that requires a user to log in. To do this I simply use Vue Router with protected routes. When the user logs in, they receive an auth token from the server which I store in local storage.
Here's a small part of router/index.js:
const ifNotAuthenticated = (to, from, next) => {
if (!store.getters.isAuthenticated) {
next()
return
}
next('/')
}
const ifAuthenticated = (to, from, next) => {
if (store.getters.isAuthenticated) {
next()
return
}
next('/login/')
}
export default new VueRouter({
routes: [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
beforeEnter: ifAuthenticated,
},
{
path: '/login/',
name: 'Login',
component: Login,
beforeEnter: ifNotAuthenticated,
}
]
})
Here's a small part of store/modules/auth.js:
const actions = {
[AUTH_REQUEST]: ({ commit }, user) => {
return new Promise((resolve, reject) => {
commit(AUTH_REQUEST);
Api.getToken(user)
.then(res => {
localStorage.setItem('user-token', res.data.token);
commit(AUTH_SUCCESS, res.data);
resolve(res);
})
.catch(err => {
commit(AUTH_ERROR, err);
reject(err);
});
})
},
[AUTH_LOGOUT]: ({ commit }) => {
return new Promise((resolve) => {
commit(AUTH_LOGOUT)
localStorage.removeItem('user-token')
resolve()
})
}
}
All very standard I think. So my question is, after a user has logged in I need to make a call to the server to retrieve the users settings. How would I go about doing this to ensure that a logged in user always has their settings available to them throughout the app (ie: its in the store).
Here's a few scenarios:
The user logs in for the first time and receives their auth token (saved in local storage). Then their settings are retrieved and saved to the store.
A user logged in yesterday. Today they don't have to log in again because their auth token is already stored in local storage. Therefore I just need to retrieve their settings no matter which page they happen to open the app on.
Essentially, I need to ensure that a users settings are downloaded either when they initially log in or when they return to the site later on but are already logged in.
You could always use a vuex dispatch. Create an action that fetches the users settings, then, from your [AUTH_REQUEST] action, on successful login, dispatch the created action.
dispatch("[FETCH_USER_SETTINGS]", res.data.id);
localStorage.setItem('user-token', res.data.token);
commit(AUTH_SUCCESS, res.data);
resolve(res);
You'd also need to include it in your params:
[AUTH_REQUEST]: ({ commit, dispatch }, user) => {
This way the action will be called for any scenario where the user logs in.

Vue/Vuex - how to stay logged in as a user after route change

As a small Dev-Team, we are about to create our own social media website just for fun.
Our login process is handled with a jwt which is stored in localStorage.
async login( { commit }, user) {
commit('auth_request');
try {
const res = await axios.post('http://localhost:3000/api/v1/users/login', user);
if (res.data.status === 'success') {
const token = res.data.token;
const user = res.data.user;
localStorage.setItem('jwt', token);
commit('auth_success', { token, user } );
toast.success(res.data.message);
router.push({ path: '/' })
}
return res;
} catch (error) {
commit('set_errors', error.response.data)
toast.error(error.response.data.message);
}
},
However as soon as we change route we getting logged out.
router.beforeEach((to, from, next) => {
// check for user is logged in on route change
next();
})
How to prevent getting logged out on route change or page reload?
Well you are committing the token and it is saved in your localstorage so you can try this way so that your router knows that you are authenticated
This is a example router
path: "/dashboard",
name: "dashboard",
meta: { requiresAuth: true },
component: () => import("../views/Dashboard.vue")
I have a meta with requiresAuth: true so you can go to your router.beforeEach and create a if statement to check if there is a token if there is a token then stay logged in if not when trying to get into the Dashboard page take me to the login page.
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
if (window.localStorage.getItem("jwt")) {
next();
} else {
next({ name: "login" });
}
}else{
next()
}
});
We are doing a if stament where we get the token with window.localStorage.getItem("jwt") if there is a token in the localstorage then we can tell our router to stay logged in and navigate to the pages that has meta: { requiresAuth: true } if we dont have the token in localstorage take us to login page
I hope this helps you out or gives you an idea how to guard your routes. If you haven't solved the problem then just comment on the answer so we can solve the problem.

Request vuejs axios after auto login completed

How to execute all other request after auto login wass passed. Code example.
axios.get('personal/' + this.$store.state.username + '/config/', {headers: { Authorization: 'Token ' + this.$store.state.idToken }})
Sometimes request that receive user data (username and id) have no time to passed and commit to state, and i receive an error that username is null at state.
I have solve that problem by add in login func to save username and id in localstorage, and after in try auto login i have next code:
tryAutoLogin ({ commit, dispatch }) {
const token = localStorage.getItem('token')
if (!token) {
return
} else {
commit('getToken', {
key: token
})
const userId = localStorage.getItem('userId')
const username = localStorage.getItem('username')
if (!userId || !username) {
dispatch('getUser')
} else {
commit('getUserData', {
id: userId,
username: username.username
})
}
}
Is this way is ok? or there is any way to stop anny request to api, till the dispatch('getUser') will be passed succesfully.
example of getUser code:
getUser ({ commit, state }) {
if (!state.idToken) {
return
}
axios.get('rest-auth/user/', {headers: { Authorization: 'Token ' + state.idToken }})
.then(res => {
localStorage.setItem('username', res.data.username)
localStorage.setItem('userId', res.data.pk)
commit('getUserData', {
id: res.data.pk,
username: res.data.username
})
})
},
Plz, don't be strict i am new in FE vue js:)
First of all, make names of getters, actions, mutations and state more clean and obvious (getUser for getters and setUser for action, for example).
I recommend to create a separated auth module (place all auth logic in this module) and use it in Vuex actions or somewhere in application.
Such module should interact with the Store via Vuex getters, setters and actions (getting and setting current user auth status, for example). It makes authentification more incapsulated and clear.
In this way you'll be able to call this module's methods from any of application component and wait for the result.
In code bellow (http_auth.js) this.$auth is separated authentification module, that can set user state in Vuex and get current status. Also it use localStorage to check for saved token (user data) and tries to authorize with saved token (tryAutoLogin in your case). On fail, it makes redirect to login page.
...
methods: {
async loadInitialData () {
if (await this.$auth.init()) {
axios.get('initial-data-url').then(res => ...)
}
}
},
created () {
this.loadInitialData()
}
...
Auth methods are Promise-based, so you just can wait for resolving or rejecting before.
If you just want to use Vuex-only solution, you should use actions to call API-requests and wrap them in Promises. Also you can dispatch some action inside of other (for example, try login with saved token inside of basic login action).
Sample code (Vuex action):
LOAD_SOME_DATA ({ commit, state, getters }, id) {
return new Promise((resolve, reject) => {
if (!id) {
router.push('/')
return reject('Invalid ID passed.')
}
return axios.get(getters.GET_SOME_URL + id).then(response => {
commit('PUSH_SOME_DATA', response.data)
return store.dispatch('PROCESS_SOME_DATA').then(result => {
return resolve(response)
}, error => {
console.error('Error loading some data: ', error)
return reject(error)
})
}, error => {
router.push('/')
return reject(error)
})
})
}
Above we wrap in promise basic api-call (axios.get(getters.GET_SOME_URL + id)), then process received data (PROCESS_SOME_DATA).
Then we can use it in router, for example (or any other part of app):
store.dispatch('LOAD_SOME_DATA', to.params.id).then(result => ...)