NuxtServerInit sets Vuex auth state after reload - javascript

I'm setting a basic authentication on a Nuxt project with JWT token and cookies to be parsed by nuxtServerInit function.
On login with email/password, works as intended, setUser mutation is triggered and the appropriate user object is stored in state.auth.user.
On reload, nuxtServerInit will get the jwt token from req.headers.cookies, call the GET method and identify user.Works like a charm.
Problem starts when I hit the /logout endpoint. state.auth.user is set to false and Im effectively logged out... but If I refresh, I'm logged in again with the previous user data. Even if my cookies are properly empty (on below code, both user and cookie are undefined after logout and refresh, as expected)
So I really don't get why is my state.auth.user is back to its initial value...
store/index.js
import Vuex from "vuex";
import auth from "./modules/auth";
import axios from "~/plugins/axios";
const cookieparser = process.server ? require("cookieparser") : undefined;
const END_POINT = "api/users";
const createStore = () => {
return new Vuex.Store({
actions: {
async nuxtServerInit({ commit, dispatch}, { req }) {
let cookie = null;
console.log(req.headers.cookie)
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie);
try {
cookie = JSON.parse(parsed.auth);
console.log("cookie", cookie)
const {accessToken} = cookie
const config = {
headers: {
Authorization: `Bearer ${accessToken}`
}
}
const response = await axios.get(`${END_POINT}/current`, config)
const user = response.data
console.log("user nuxt server init", user)
await commit('setUser', user)
} catch (err) {
// No valid cookie found
console.log(err);
}
}
}
},
modules: {
auth
}
});
};
export default createStore;
modules/auth.js
import axios from "~/plugins/axios";
const Cookie = process.client ? require("js-cookie") : undefined;
const END_POINT = "api/users";
export default {
state: {
user: null,
errors: {}
},
getters: {
isAuth: state => !!state.user
},
actions: {
login({ commit }, payload) {
axios
.post(`${END_POINT}/login`, payload)
.then(({ data }) => {
const { user, accessToken } = data;
const auth = { accessToken };
Cookie.set("auth", auth);
commit("setUser", user);
})
.catch(e => {
const error = e;
console.log(e);
commit("setError", error);
});
},
logout({ commit }) {
axios
.post(`${END_POINT}/logout`)
.then(({ data }) => {
Cookie.remove("auth");
commit("setUser", false);
})
.catch(e => console.log(e));
},
},
mutations: {
setUser(state, user) {
state.user = user;
},
setError(state, errors) {
state.errors = errors;
}
}
};

The way I logout my user is by creating a mutation called clearToken and commit to it in the action :
State :
token: null,
Mutations :
clearToken(state) {
state.token = null
},
Actions :
logout(context) {
context.commit('clearToken')
Cookie.remove('token')
}
This way, you token state revert back to null.

Related

React GraphQL authentication flow with userContext

I have a React application which allows the user to login via a form then navigates to the home page. I need the App component to provide the user down to its children.
App.js
function App() {
const [token, setToken] = useState(null)
const userQuery = useQuery(USER) // Gets user from GraphQL, using token in the request context
...
useEffect(() => {
setToken(localStorage.getItem("user-token"))
}, [])
return (
<UserContext.Provider value={userQuery.data}>
...
</UserContext.Provider>
)
}
LoginForm.js
const LoginForm = () => {
...
const [login, result] = useMutation(LOGIN, {
onError: (error) => {
console.log("error :>> ", error)
setError(error.graphQLErrors[0].message)
},
})
useEffect(() => {
if (result.data) {
const token = result.data.login.value
localStorage.setItem("user-token", token)
navigate("/")
}
}, [result.data])
...
}
index.js
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("user-token")
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
When the login form submits the GraphQL login query is executed. Then when the result is received the token is set in local storage and it navigates back to the main page. The problem is that the app's user query receives a null user because the request wasn't sent with the new storage token. How can I get it to do this without refreshing the page?

How to access store state from a different store module in vuex?

EDIT: I found the solution which simply was passing the token as an argument in the mounted() function in my vue module:
mounted(){
this.$store.dispatch("fetchData", this.$store.state.auth.user.token)
and in the store I use my async function like this:
async fetchData({commit}, id)
I want to make API calls from my store but I also have the user information (userId, jwttoken) necessary for those API calls stored in the same place .
this is my store:
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import auth from "./modules/auth";
import prod from "./modules/prod";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
auth,
prod
},
plugins: [createPersistedState()],
});
this is my authentication module in the store:
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
const actions = {
async Register({ dispatch }, form) {
await axios.post("user/signup", form);
await dispatch("LogIn", form);
},
async LogIn({ commit }, User) {
const response = await axios.post("user/login", User);
console.log(response)
await commit("setUser", response.data);
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
const mutations = {
setUser(state, user) {
state.user = user;
},
logout(state) {
state.user = null;
},
};
export default {
state,
getters,
actions,
mutations,
};
and this is where I want to call it in a different store file, with the line this.$store.state.auth.user.token to get the token necessary for the API call:
//store/modules/prod.js
import axios from "axios";
const state = {
products: null,
};
const actions = {
async fetchData({commit}) {
const headers = {
authorization: "Bearer " + this.$store.state.auth.user.token,
};
let resp = await axios.get("product", {
headers
});
await commit("SET_PRODUCTS", resp.data)
}
};
const mutations = {
SET_PRODUCTS(state, products) {
state.products = products
}
};
export default {
state,
actions,
mutations,
};
which I call in a component like this:
products(){
return this.$store.state.products
}
},
mounted(){
this.$store.dispatch("fetchData")
However, the request from the store never sends to the API because it cannot read the token. I get "cannot read undefined (reading state)
I've also tried importing it like this but it says token is undefined:
import auth from "../modules/auth"
const headers = {
authorization: "Bearer " + auth.user.token,
};
I've read online that you should access the store from non vue modules through Getters but I couldn't manage to make it work either, would this be the correct way to go though?
Actions take a context object as parameter, you can use it to access rootState. Check documentation for other available options.
https://vuex.vuejs.org/api/#actions
const actions = {
async fetchData({commit, rootState}) {
const headers = {
authorization: "Bearer " + rootState.auth.user.token,
};
let resp = await axios.get("product", {
headers
});
await commit("SET_PRODUCTS", resp.data)
}
};

Nuxt middleware to check if user is logged in not working

I am trying to check if a user is authenticated and redirect them depending on the page they are in. for example if the user is logged in and they try to visit the login or signup page, they should be redirected. I have a middleware for that.
when I log in the user, the authenticateUser action runs and the user is created, when I check my cookies and local storage on the browser, I see that it is set correctly, but when I visit the login page after logging in, it doesn't redirect me.
middleware/altauth.js
export default function (context) {
console.log(context.store.getters('profile/isAuthenticated'))
if (context.store.getters.isAuthenticated) {
context.redirect('/')
}
}
also the token is both saved using Cookies and local storage and is persistence is through this middleware
middleware/checkauth.js
export default function (context) {
if(context.hasOwnProperty('ssrContext')) {
context.store.dispatch('profile/initAuth', context.ssrContext.req);
} else {
context.store.dispatch('profile/initAuth', null);
}
}
and below are the values for my store
import Cookie from 'js-cookie';
export const state = () => ({
token: null,
})
export const mutations = {
setToken(state, token) {
state.token = token
},
clearToken(state) {
state.token = null
}
}
export const actions = {
async authenticateUser(vuexContext, authData) {
let authUrl = 'https://look.herokuapp.com/signup/'
if (authData.isLogin) {
authUrl = 'https://look.herokuapp.com/login/'
}
return this.$axios
.$post(authUrl, authData.form)
.then(data => {
console.log(data);
const token = data.token
vuexContext.commit('setToken', token)
localStorage.setItem("token", token)
Cookie.set('jwt', token);
})
.catch(e => console.log(e))
},
initAuth(vuexContext, req) {
let token
if (req) {
if (!req.headers.cookie) {
return;
}
const jwtCookie = req.headers.cookie
.split(';')
.find(c => c.trim().startsWith('jwt='));
if (!jwtCookie) {
return;
}
token = jwtCookie.split('=')[1];
} else {
token = localStorage.getItem('token');
if (!token) {
return;
}
}
vuexContext.commit('setToken', token);
}
}
export const getters = {
isAuthenticated(state) {
return state.token != null;
},
}
please help, i don't know what the problem can be
Here is a basic but full example for auth system in SSR nuxt
You will need two apis for this, one will return token info with user info, and the other will return user info only.
for example
POST http://example.com/api/auth/authorizations
{
token: 'abcdefghijklmn',
expired_at: 12345678,
user: {
name: 'Tom',
is_admin: true
}
}
// this need authed
GET http://example.com/api/auth/user
{
name: 'Tom',
is_admin: true
}
nuxt.config.js
plugins:[
'~plugins/axios',
],
buildModules: [
'#nuxtjs/axios',
],
router: {
middleware: [
'check-auth'
]
},
./pages/login.vue
<template>
<form #submit.prevent="login">
<input type="text" name="username" v-model="form.username">
<input type="password" name="password" v-model="form.password">
</form>
</template>
<script type="text/javascript">
export default{
data(){
return {
form: {username: '', password: ''}
}
},
methods: {
login(){
this.$axios.post(`/auth/authorizations`, this.form)
.then(({ data }) => {
let { user, token } = data;
this.$store.commit('auth/setToken', token);
this.$store.commit('auth/updateUser', user);
this.$router.push('/');
})
}
}
}
</script>
store/index.js
const cookieFromRequest = (request, key) => {
if (!request.headers.cookie) {
return;
}
const cookie = request.headers.cookie.split(';').find(
c => c.trim().startsWith(`${key}=`)
);
if (cookie) {
return cookie.split('=')[1];
}
}
export const actions = {
nuxtServerInit({ commit, dispatch, route }, { req }){
const token = cookieFromRequest(req, 'token');
if (!!token) {
commit('auth/setToken', token);
}
}
};
middleware/check-auth.js
export default async ({ $axios, store }) => {
const token = store.getters['auth/token'];
if (process.server) {
if (token) {
$axios.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete $axios.defaults.headers.common.Authorization;
}
}
if (!store.getters['auth/check'] && token) {
await store.dispatch('auth/fetchUser');
}
}
store/auth.js
import Cookie from 'js-cookie';
export const state = () => ({
user: null,
token: null
});
export const getters = {
user: state => state.user,
token: state => state.token,
check: state => state.user !== null
};
export const mutations = {
setToken(state, token){
state.token = token;
},
fetchUserSuccess(state, user){
state.user = user;
},
fetchUserFailure(state){
state.user = null;
},
logout(state){
state.token = null;
state.user = null;
},
updateUser(state, { user }){
state.user = user;
}
}
export const actions = {
saveToken({ commit }, { token, remember }){
commit('setToken', token);
Cookie.set('token', token);
},
async fetchUser({ commit }){
try{
const { data } = await this.$axios.get('/auth/user');
commit('fetchUserSuccess', data);
}catch(e){
Cookie.remove('token');
commit('fetchUserFailure');
}
},
updateUser({ commit }, payload){
commit('updateUser', payload);
},
async logout({ commit }){
try{
await this.$axios.delete('/auth/authorizations');
}catch(e){}
Cookie.remove('token');
commit('logout');
}
}
plugins/axios.js
export default ({ $axios, store }) => {
$axios.setBaseURL('http://example.com/api');
const token = store.getters['auth/token'];
if (token) {
$axios.setToken(token, 'Bearer')
}
$axios.onResponseError(error => {
const { status } = error.response || {};
if (status === 401 && store.getters['auth/check']) {
store.commit('auth/logout');
}
else{
return Promise.reject(error);
}
});
}
Then you can do what you want in your middleware, such as check auth
middleware/auth.js
export default function ({ store, redirect }){
if (!store.getters['auth/check']) {
return redirect(`/login`);
}
}

Nextjs auth with Firebase not sharing cookie accross pages

I'm trying to create auth using Firebase and NextJS
I followed this tutorial: [https://colinhacks.com/essays/nextjs-firebase-authentication][1].
Whenever I access an SSR authenticated page the token is deleted from the front end, firebase gives an error stating FirebaseAuthError: First argument to verifyIdToken() must be a Firebase ID token string..
It seems like the cookie is deleted whenever SSR is run because Firebase see's the cookie as invalid. I've tried setting the cookie using js-cookie and using nookies.
I've tried different cookie frameworks for setting cookies, the NextJS native method of fetching cookies SSR and nookies for fetching cookies SSR.
auth.js
const AuthContext = createContext<{ user: firebaseClient.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebaseClient.User | null>(null);
console.log("User is ", user)
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).nookies = nookies;
}
return firebaseClient.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
// Cookies.set("token", "");
nookies.set(null, "token", "",);
return;
}
const token = await user.getIdToken();
// Cookies.set("token", token);
nookies.set(null, "token", token, {
path: "/",
encode: (v) => v,
});
setUser(user);
});
}, []);
useEffect(() => {
const handle = setInterval(async () => {
console.log(`refreshing token...`);
const user = firebaseClient.auth().currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
export const useAuth = () => {
return useContext(AuthContext);
};
Authenticated page
export const getServerSideProps = async (ctx, req) => {
try {
const cookies = nookies.get(ctx);
// Fails here, never accepts token
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
console.log("Token is:", token)
return {
props: {
data: "Worked!"
},
};
} catch (err) {
return {
props: {
data: "Firebase failed!"
},
};
}
};
const Revisions = ({ data, ctx }) => {
const { user } = useAuth();
return (
<Layout>
<Container>
{/* always returns no user signed in */}
<p>{`User ID: ${user ? user.uid : 'no user signed in'}`}</p>
<p>{data}</p>
</Container>
</Layout>
);
};
EDIT: Additional information
On the loginpage cookie is set on the front end. I can see this cookie in my Applications tab.
[![Application tab chrome showing token and value][2]][2]
I have verified that localhost with the correct port is permitted through the firebase console.
The token seems to get deleted at this line
const token = await firebaseAdmin.auth().verifyIdToken(cookie);
I have tried using JSON parse, both with JSON.stringify first and just straight JSON parsing the data.
[1]: https://colinhacks.com/essays/nextjs-firebase-authentication
[2]: https://i.stack.imgur.com/p8MES.png

Cannot initialise Vuex store from localStorage for my Auth in Nuxt

I'm using Nuxt.js and I'm trying to make my own authentication. It works fine but when I refresh the page the state is go back to initial data so I tried to initialise Vuex store from localStorage like this:
export const state = () => ({
status: '',
token: localStorage.getItem('token') || '',
loggedInUser: localStorage.getItem('user') || '',
})
but it give me this error localStorage is not defined but localStorage.setItem works fine in actions
This is the full code:
import axios from 'axios'
export const state = () => ({
status: '',
token: localStorage.getItem('token') || '',
loggedInUser: localStorage.getItem('user') || '',
})
export const getters = {
status (state) {
return state.status
},
authenticated (state) {
return !!state.token
},
token (state) {
return state.token
},
loggedInUser (state) {
return state.loggedInUser
},
}
export const mutations = {
auth_request(state) {
state.status = 'loading'
},
auth_success(state, token) {
state.status = 'success'
state.token = token
},
auth_error(state) {
state.status = 'error'
},
logout(state) {
state.status = ''
state.token = ''
state.loggedInUser = {}
},
auth_success2 (state, loggedInUser) {
state.loggedInUser = Object.assign({}, loggedInUser)
}
}
export const actions = {
login({commit}, data) {
return new Promise((resolve, reject) => {
commit('auth_request')
axios.post('http://127.0.0.1:8000/api/login', data)
.then((res) => {
const loggedInUser = Object.assign({}, res.data.data)
const token = res.data.meta.token
localStorage.setItem('token', token)
localStorage.setItem('user', loggedInUser.name)
axios.defaults.headers.common['Authorization'] = 'Bearer '+ token
commit('auth_success', token)
commit('auth_success2', loggedInUser)
this.$router.push('/')
resolve(res)
})
.catch((error) => {
commit('auth_error')
console.log(error)
reject(error)
})
})
}
}
You didn't include where this error is being thrown, but I'm going to assume it's in your server logs.
What's happening is Nuxt is initializing itself server side, begins to set up the store, and hits the localStorage declaration. The server does not have localstorage, so this will fail.
To get around this, I'd suggest using a plugin, with the .client suffix, and fetch the values from localStorage during the client side initialization:
// the .client suffix is required here to tell nuxt to only run this client side.
// ~/plugins/vuex-init.client.js
export default ({ store }) => {
const token = localStorage.getItem('token') || ''
const loggedInUser = localStorage.getItem('user') || ''
store.commit('setToken', token)
store.commit('setUser', user)
}
If you don't want to do the work yourself, I've used this before and have had great results with it.

Categories

Resources