Authentication work in the not appropriate way Vue js - javascript

I have guarded routes, and they depend on the value in the store.
loginUser() { // i call this function when user press "Login"
this.$http.get('http://localhost:3000/users')
.then(resp => {
return resp.json()
})
.then(resp => resp.filter(item => item.email === this.email && item.password === this.password))
.then(res => res.length > 0 ? (this.$router.push('/'), this.$store.commit('logUser')) : this.visible = true); // here i change value in the store
}
// Store
userLogged: false // state
mutations: {
logUser(state) { // i call this function when loginUser() function calls
state.userLogged = true
}
}
// router index.js
{
path: '/',
name: 'Home',
component: Home,
beforeEnter: AuthGuard
}
export default function (to, from, next) { // this is AuthGuard function
if (store.getters.getUser) { // get value from store
next();
} else {
next('/login')
}
}
// store
getters: { // get value
getUser(state) {
return state.userLogged
}
}
This works in the not appropriate way. I have to click the login button twice because getUser() get the state and logUser() changing userLogged simultaneously i.e. getUser() return false and when i am clicking second time its true. How can i fix this?

Related

Vue: How to use Per-Route Guard with Vuex getter?

Similar to the way that we handle with isAuthenticate function to check if user has properly authenticated, I'm trying to inspect in my store.
const state = {
cliente: []
};
const getters = {
//Verificar Regra
CHECK_CLIENTE_STATE: (state) => {
return state.cliente
}
}
const actions = {
FETCH_DADOS({ commit }, obj) {
return fetch(`http://localhost:3030/pessoas/informacao/${obj['data']}`)
.then(response => response.json())
.then(data => commit('SetCliente', data))
.catch(error => console.log(`Fetch: ${error}`))
}
}
const mutations = {
SetCliente(state, cliente) {
state.cliente = cliente
}
}
login page,
methods:{
fetch(){
this.$store.dispatch("FETCH_DADOS",{'data':'12345'})
this.$router.push('/')
}
}
At the first fetch click, I inspect Vuex, apparently it is working.
Routes:
const routes = [{
path: '/',
beforeEnter: (to, from, next) => {
if (store.getters.CHECK_CLIENTE_STATE == '') {
next('/login')
}
next();
},
component: () =>
import ('../views/Home')
},
{
path: '/login',
component: () =>
import ('../views/Login')
}
]
Well, in console.log at the first fetch click, I receive this error, but in vuex as shown above, the store is filled.
Uncaught (in promise) Error: Redirected when going from "/login" to
"/" via a navigation guard.
Why just in the second click is it redirected to home, not in the first?
Updating
Trying a new approach in router.js
path: '/',
beforeEnter: (to, from, next) => {
console.log(!store.getters.CHECK_CLIENTE_STATE.length)
if (!store.getters.CHECK_CLIENTE_STATE.length) {
next('/login')
}
next();
},
component: () =>
import ('../views/Home')
But again, the first fetch is TRUE and the second FALSE, in the second I'm redirected to /home
The router is being directed before the data is loaded. Wait for it:
methods:{
async fetch(){
await this.$store.dispatch("FETCH_DADOS",{'data':'12345'})
this.$router.push('/')
}
}

How to authenticate Nuxt on server side?

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!

Keeping User state when refreshing or changing the page in vuex

As you can see my user state is null at the beginning but when the user logs in, I need to user id for my action called setFavoriteCrypto
So I can retrieve the specific data that is related to the user connected but since vuex initialize the state every time there's a refresh of the page. the user becomes null
I know there's a package called persist state which is something I could work with.
But I would like to know if there's a different way of managing my user connected without depending on a package?
I am using node.js as my backend for this app.
import Api from '#/services/Api'
import axios from 'axios'
import router from '#/router'
const state = {
localStorageToken: localStorage.getItem('user-token') || null,
token: null,
user: null,
favoriteCrypto: []
}
const mutations = {
setToken(state, token) {
state.localStorageToken = token
state.token = token
},
setUser(state, user) {
state.user = user
},
setFavoritecrypto(state, crypto) {
state.favoriteCrypto = crypto
}
}
const actions = {
setToken({commit}, token) {
commit('setToken', token)
},
setUser({commit}, user) {
commit('setUser', user)
},
setFavoriteCrypto({commit}, token) {
if(state.user) {
return Api().get('getuserfavoritescoins', {
params: {
userId: state.user.id
}
})
.then(response => response.data.coins)
.then(cryptoFav => {
commit('setFavoritecrypto', cryptoFav)
})
}
else{
console.log("there's not user state")
}
},
loginUser({commit}, payload) {
return Api().post('login', payload)
.then(response => {
commit('setUser', response.data.user)
commit('setToken', response.data.token)
localStorage.setItem('user-token', response.data.token)
axios.defaults.headers.common['Authorization'] = response.data.token
router.push('/main').catch(e => {})
})
},
logoutUser({commit, dispatch}) {
localStorage.removeItem('user-token')
commit('setUser', null)
commit('setToken', null)
delete axios.defaults.headers.common['Authorization']
}
}
const getters = {
isAuthenticated: state => !!state.localStorageToken,
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

ReactJS - Proper way to get a result of dispatch

For async requests, I'm using redux-saga.
In my component, I call an action to recover the user password, it its working but I need a way to know, in my component, that the action I dispatched was successfully executed, like this:
success below is returning:
payload: {email: "test#mail.com"}
type: "#user/RecoverUserPasswordRequest"
__proto__: Object
My component:
async function onSubmit(data) {
const success = await dispatch(recoverUserPasswordRequestAction(data.email))
if (success) {
// do something
}
}
My actions.js
export function recoverUserPasswordRequest(email) {
return {
type: actions.RECOVER_USER_PASSWORD_REQUEST,
payload: { email },
}
}
export function recoverUserPasswordSuccess(email) {
return {
type: actions.RECOVER_USER_PASSWORD_SUCCESS,
payload: { email },
}
}
export function recoverUserPasswordFailure() {
return {
type: actions.RECOVER_USER_PASSWORD_FAILURE,
}
}
My sagas.js
export function* recoverUserPassword({ payload }) {
const { email } = payload
try {
const response = yield call(api.patch, 'user/forgot-password', {
email
})
// response here if success is only a code 204
console.log('response', response)
yield put(recoverUserPasswordSuccess(email))
} catch (err) {
toast.error('User doesnt exists');
yield put(recoverUserPasswordFailure())
}
}
export default all([
takeLatest(RECOVER_USER_PASSWORD_REQUEST, recoverUserPassword),
])
In my reducer.js I dont have nothing related to recover the user's password, like a RECOVER_USER_PASSWORD_SUCCESS because like I said, the api response from my saga is only a code 204 with no informations
You should treat this as a state change in your application.
Add a reducer that receives these actions RECOVER_USER_PASSWORD_SUCCESS or RECOVER_USER_PASSWORD_FAILURE, then updates the store with information about request status. For example:
const initialState = {
email: null,
status: null,
}
const recoverPasswordReducer = (state=initialState, action) => {
//...
if (action.type === actions.RECOVER_USER_PASSWORD_SUCCESS) {
return {...initialState, status: True }
}
if (action.type === actions.RECOVER_USER_PASSWORD_SUCCESS) {
return {...initialState, status: False }
}
return state;
}
You can later have status as one of the fields selected in mapStateToProps when connect the component that needs to know about the status of the operation to the store.
function mapStateToProps(state) {
return {
/* ... other fields needed from state */
status: state.status
}
}
export connect(mapStateToProps)(ComponentNeedsToKnow)

Vue - Mixin on page refresh

When I navigate using vue router, everything works. Ex: I click on button using #click=login(). Above the login method
methods: {
login () {
this.$router.push('/admin')
}
So vue redirect user to '/Admin' page and the router calls beforeEach event where I check if the page has meta attribute to validate user access
Then on my router/index.js
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
Routes
]
})
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAdminAuth) {
let app = router.app.$data || {isAuthenticated: false}
if (app.isAuthenticated) {
// already signed in, we can navigate anywhere
next()
} else if (to.matched.some(record => record.meta.requiresAdminAuth)) {
// authentication is required. Trigger the sign in process, including the return URI
console.log(router.app.hello)
let authenticate = router.app.authenticate
authenticate(to.path).then(() => {
console.log('authenticating a protected url:' + to.path)
next()
})
} else {
// No auth required. We can navigate
next()
}
} else {
next()
}
})
To validade the user I'm usgin global method and this works. But when user navigate to '/Admin' from URL, not from my button, the global variable is undefined. So I tried to use Mixin, but the result was the same.
Here is my main.js where I specify global method and the mixin:
var myMixin = {
methods: {
hello: function () {
console.log('Bem-vindo ao mixin!')
}
}
}
const globalData = {
isAuthenticated: false,
user: '',
mgr: mgr
}
const globalMethods = {
async authenticate (returnPath) {
const user = await this.$root.getUser()
if (user) {
this.isAuthenticated = true
this.user = user
} else {
await this.$root.signIn(returnPath)
}
},
async getUser () {
try {
let user = await this.mgr.getUser()
console.log(user)
return user
} catch (err) {
console.log(err)
}
},
signIn (returnPath) {
returnPath ? this.mgr.signinRedirect({ state: returnPath })
: this.mgr.signinRedirect()
}
}
/* eslint-disable no-new */
new Vue({
mixins: [myMixin],
el: '#app',
router,
store,
template: '<App/>',
components: { App },
data: globalData,
methods: globalMethods
})
How can I do that? Execute a defined method when user navigate by URL?

Categories