0

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

enter image description here

Network tab

enter image description here

Axios Error

enter image description here

details of request/response

enter image description here

enter image description here

Response of login

enter image description here

Othmane
  • 123
  • 1
  • 9
  • I'm pretty sure that if you check your network tab (browser devtools), you'll see that your 401 error is related to CORS. If so, check a [solution here](https://stackoverflow.com/a/72211930/8816585). – kissu Nov 08 '22 at 15:29
  • @kissu but when I refresh the page I get the data after getting that error. And I already enabled the cors for the client in my backend – Othmane Nov 08 '22 at 15:32
  • What do you see in your devtools alongside the error? For me, it's a CORS issue because you don't have it initially (while having a running server), then it happens because you're doing client-side navigation. Does it happen if you come back to the page with a client-side navigation (probably is). – kissu Nov 08 '22 at 15:35
  • I just saved a new user like `await axios.post('/users', user);` `await router.push('/');` to return back to the component and the results show just fine – Othmane Nov 08 '22 at 15:38
  • @kissu I updated my question and added the devtools error – Othmane Nov 08 '22 at 15:41
  • What do you have in your network tab? There may be more details there. Also, what is inside of the Axios error message object? May give more details. Anyway, [a 401](https://http.cat/401) is unauthorized so you're missing something in your credentials/rights I guess. – kissu Nov 08 '22 at 15:52
  • @kissu I updated my answer but why when I reload the page I don't get the error? This doesn't make sense to me – Othmane Nov 08 '22 at 15:58
  • What if you click on `users` in the network tab, no more details? Do you have the same issue if you try with Postman/Insomnia? Do you have a public github repo? – kissu Nov 08 '22 at 16:00
  • @kissu I updated my answer and commit the project to github https://github.com/boumediane1/demo-vue – Othmane Nov 08 '22 at 16:05
  • @kissu postman and insomnia work just fine – Othmane Nov 08 '22 at 16:08
  • Got any public test credentials to share? – kissu Nov 08 '22 at 16:14
  • But the server is running in my local machine – Othmane Nov 08 '22 at 16:15
  • Oh, won't be able to help further haha. As for the network tab, I was referring to preview/response. If it works with Postman, then it may be a CORS issue still. What do you have on backend's logs? The 401's reason should be logged there. – kissu Nov 08 '22 at 16:17
  • 1
    @kissu I attached the previous response again. All requests work fine with Postman. Thank you for your help anyway :) – Othmane Nov 08 '22 at 16:22
  • the `/users` request is a get, which wouldn't send preflight. Also it returns a 401, so it seems indicative of the authentication being incorrect. If works on refresh, it seems indicative of a race condition. – Daniel Nov 08 '22 at 16:31
  • @Daniel I tried a dummy api and it works but not in my api and it was working like a while ago. I'm not sure what's going on – Othmane Nov 08 '22 at 16:35

1 Answers1

1

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
    }
},
Daniel
  • 34,125
  • 17
  • 102
  • 150
  • But how should it wait? I tried to set a timeout but not working – Othmane Nov 08 '22 at 16:27
  • one approach is to use `watch` instead of `onMounted`, updated answer with options API example – Daniel Nov 08 '22 at 16:28
  • My login is in another file `auth.store.js` here's the public github repo – Othmane Nov 08 '22 at 16:31
  • I recommend validating first if that's the case. To do that, you could add a `setTimeout` before making the `users` request. If it works then my assertion is likely correct and then you can figure out how to solve for that. If not, then the issue may be something else. But it's important to validate first before you start looking into implementing a fix. Fixing the problem, however may fall out of the scope of the question – Daniel Nov 08 '22 at 16:42
  • I found a hard time translating your code to the composition API but I tried the setTimeout like ```onMounted( async () => { setTimeout(() => {}, 5000); const response = await axios.get('/users'); users.value = response.data; })``` but still getting the error – Othmane Nov 08 '22 at 16:52
  • your setTimeout is non-blocking and its use has no effect. you should have the code inside the function `setTimeout(async () => {***CODE GOES HERE***}, 5000)` – Daniel Nov 08 '22 at 16:56
  • sorry for the delay. I tried it but it waits 5 seconds and the result is the same. The error still occurs – Othmane Nov 08 '22 at 17:34
  • I passed the header along with the request ```await axios.get('/users', { headers: { 'Authorization': `Bearer ${authStore.user.accessToken}` } });``` and it's working kinda dirty but it'll do for now – Othmane Nov 08 '22 at 17:44
  • interesting. does the API use cookies? The code doesn't show how you handle the authentication token. – Daniel Nov 08 '22 at 19:22
  • sorry for the delay. Here's how I handled the authentication when I first submitted this question. https://github.com/boumediane1/demo-vue/blob/main/src/helpers/axios.js (I'm using JWT) then I sent the Authentication header along with each request but it was bad practice then I did this instead – Othmane Nov 11 '22 at 15:38
  • ```import axios from "axios"; import {useAuthStore} from "../stores/auth.store.js"; axios.defaults.baseURL = import.meta.env.VITE_API_URL; axios.interceptors.request.use( config => { const authStore = useAuthStore(); if (authStore.user) { config.headers['Authorization'] = `Bearer ${authStore.user.accessToken}`; } return config; } )``` – Othmane Nov 11 '22 at 15:38
  • Thanks for the help. I learned a lot from your answer – Othmane Nov 11 '22 at 15:38
  • I had a look at the code and see what caused this behaviour, have a look at the updated answer – Daniel Nov 11 '22 at 17:18
  • Thanks Dan. You explained it very well – Othmane Nov 11 '22 at 17:36