Laravel Sanctum/Vuex Uncaught Error Vue Router Navigation Guard - laravel

Can anyone advise on this issue?
I've got a Laravel app with a Vue front-end, connecting to the API using Laravel Sanctum. (within the same application) I'm trying to set up the authentication guards so routes can only be accessed after authentication with the API.
I'm connecting through the state action like so:
async getAuthenticatedUser({ commit }, params) {
await axios.get('api/auth-user', { params })
.then(response => {
commit('SET_AUTHENTICATED', true)
commit('SET_AUTHENTICATED_USER', response.data)
localStorage.setItem('is-authenticated', 'true')
return Promise.resolve(response.data)
})
.catch(error => {
commit('SET_AUTHENTICATED', false)
commit('SET_AUTHENTICATED_USER', [])
localStorage.removeItem('is-authenticated')
return Promise.reject(error.response.data)
})
},
The state authenticated property is set as follows:
const state = () => ({
authenticated: localStorage.getItem('is-authenticated') || false,
authUser: [],
})
I have the following guard checking the auth. If I'm not signed in the app correctly redirects me back to the login screen when I access a route with the requiresAuth attribute.
However when I attempt to log in, I get Redirected when going from "/login" to "/dashboard" via a navigation guard.
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (store.getters["Auth/isAuthenticated"]) {
next()
return
}
next('/login')
}
if (to.matched.some((record) => record.meta.requiresVisitor)) {
if (! store.getters["Auth/isAuthenticated"]) {
next()
return
}
next('/dashboard')
}
next()
})

If it's safe to ignore, and you are using vue-router ^3.4.0, you can do:
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter
...
this.$router.push(fullPath).catch(error => {
if (!isNavigationFailure(error, NavigationFailureType.redirected)) {
throw Error(error)
}
})

Related

Strapi returns 404 for custom route only when deployed to Heroku

I have created a custom route in Strapi v4 called "user-screens". Locally I hit it with my FE code and it returns some data as expected. However when I deploy it to Heroku and attempt to access the endpoint with code also deployed to Heroku it returns a 404. I've tailed the Heroku logs and can see that the endpoint is hit on the server side, but the logs don't give anymore info other than it returned a 404.
I am doing other non custom route api calls and these all work fine on Heroku. I am able to auth, save the token, and hit the api with the JWT token and all other endpoints return data. This is only happening on my custom route when deployed to Heroku. I've set up cors with the appropriate origins, and I am wondering if I need to add something to my policies and middlewares in the custom route. I have verified the permissions and verified the route is accessible to authenticated users in the Strapi admin.
Here is my route:
module.exports = {
routes: [
{
method: "GET",
path: "/user-screens",
handler: "user-screens.getUserScreens",
config: {
policies: [],
middlewares: [],
},
},
],
};
And my controller:
"use strict";
/**
* A set of functions called "actions" for `user-screens`
*/
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
ctx.send(result);
});
},
};
For anyone facing this, the answer was to change how I returned the ctx response from a 'send' to a 'return' from the controller method. I am not sure why this works locally and not on Heroku, but this fixes it:
New controller code:
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
return strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
return result;
})
.catch((error) => {
return error;
});
},
};

Cypress using actions from Pinia Vue3

I was learning some cypress from this video: https://www.youtube.com/watch?v=03kG2rdJYtc
I'm interested with he's saying at 29:33: "programatic login"
But he's using vue2 and Vuex.
My project is created with Vite and the state management is Pinia.
So how can I do a programatic login using the pinia action?
For example the welcome logged in user should see dashboard:
describe('Welcome', () => {
it('logged in user should visit dashboard', () => {
// login
cy.visit('/')
cy.url().should('contain', '/dashboard')
})
})
And my userStore:
export const useUserStore = defineStore({
id: 'user',
state: () => ({
username: ref(useLocalStorage('username', null)),
}),
getters: {
isLoggedIn: (state) => state.username !== null,
},
actions: {
login(username, password) {
return useAuthLoginService(username, password)
.then((response) => {
this.username = response.username
})
.catch((error) => {
return Promise.reject(new Error(error))
})
},
},
})
How can I call the login action on the cypress test?
For now as a workaround I'm writing on a localstorage like:
localStorage.setItem('username', 'user')
And it works fine, because userStore catch this item from localstorage and passes like it's logged in... But I don't like this solution, seems fragile, and I'd like to use the action which is made for login users.
Another thing I tried is adding the app variable inside window but it doesn't work for me... don't understand why...
on main.js
The video shows that code:
const vue = new Vue({...})
if(window.Cypress){
window.app = app
}
In my case it's:
const app = createApp(App)
if(window.Cypress){
window.app = app
}
But in cypress tests the window.app it's undefined... I don't know how I would access to userStore using this... like it was vuex.
Using the Pinia demo app as an example:
The store is initialized in App.vue. Add a reference to the newly created store(s) for Cypress to use
export default defineComponent({
components: { Layout, PiniaLogo },
setup() {
const user = useUserStore()
const cart = useCartStore()
if (window.Cypress) {
window.store = {user, cart) // test can see window.store
}
...
In the test
let store;
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`)
.then(win => store = win.store) // get app's store object
})
it('works', () => {
cy.wait(500) // wait for the JS to load
.then(() => store.cart.addItem('Cypress test item')) // invoke action
.then(() => {
const item1 = store.cart.items[0] // invoke getter
cy.wrap(item1)
.should('have.property', 'name', 'Cypress test item') // passes
})
The login action is asynchronous, so return the promise to allow Cypress to wait.
// user.js
async login(user, password) {
const userData = await apiLogin(user, password)
this.$patch({
name: user,
...userData,
})
return userData // this returns a promise which can awaited
},
// main.spec.js
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store before login
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging in
store.user.login('ed', 'ed').then(() => { // wait for API call
cy.wrap(store.user.name).should('eq', 'ed')
})
})
})
Alternatively, wait for the name to change on the page
// main.spec.js
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging on
store.user.login('ed', 'ed')
cy.contains('Hello ed') // waits for name on page to change
.then(() => {
cy.wrap(store.user.name).should('eq', 'ed')
})
})

Inject token from auth0 to React-admin Hasura data provider

This example demonstrates how to use Auth0 with react-admin. It is working as expected.
We are trying to adjust it so it will use the hasura data provider. We've created a new file dataProvider.js that will construct the data-provider:
import buildHasuraProvider from "ra-data-hasura";
import { ApolloClient, InMemoryCache } from "#apollo/client";
export const initDataProvider = async (token) => {
const client = new ApolloClient({
uri: process.env.REACT_APP_GRAPHQL_URI,
headers: {
Authorization: `Bearer ${token}`,
},
cache: new InMemoryCache(),
});
const dataProvider = await buildHasuraProvider({ client });
return dataProvider;
};
However, we are missing the JWT token which is created as part of the Auth0 authentication process. We do not know how to get the token in order to initialize the data provider with it. Does react-admin know how to do it on its own? if not, how do we access the JWT token to do it ourselves manually?
This is the authProvider source-code:
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
redirect_uri: authConfig.redirectURI,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect()
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently()
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};
It is unclear to us if there is a point where we can extract the token from.
getTokenSilently should give you back the token.
You'll have to structure your React app such that you have access to the result of this method before you construct your data provider.

Vue js beforeRouteEnter

I am using laravel and vuejs for my app and every user has a role. Now I want to restrict users to access vue routes by their roles by redirecting them to another route if they can't access the page. I am using beforeRouteEnter for vuejs but I am getting an error
data() {
return{
role: '',
}
},
mounted() {
axios.post('/getRole')
.then((response) => this.role = response.data)
.catch((error) => this.errors = error.response.data.errors)
},
beforeRouteEnter (to, from, next) {
if (this.role === 'Admin') {
next()
}else {
next('/')
}
}
I am getting this error
app.js:72652 TypeError: Cannot read property 'role' of undefined
at beforeRouteEnter (app.js:90653)
at routeEnterGuard (app.js:72850)
at iterator (app.js:72690)
at step (app.js:72464)
at runQueue (app.js:72472)
at app.js:72726
at step (app.js:72461)
at app.js:72465
at app.js:72711
at app.js:72539
you can't use this in beforeRouteEnter function,because before the route updates, your component's vm does not exist.
in consideration of you put role information in your vm, you can use next to get your vm:
beforeRouteEnter(to, from, next) {
next(vm => {
// put your logic here
if (vm.role === 'Admin') {
}
})
}
actually,you should use the Global Router Guard ,and put your role information in global area,so that your can redirect your routing before your target component created

How to make the delay when a user check on Vue.js?

I have SPA application on Vue.js + Laravel. Authorization logic, completely delegated to Laravel app. However, i need check auth status, when routing has changed. I create small class, which responsible for it.
export default {
user: {
authenticated : false
},
check: function(context) {
context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) =>{
console.log(response)
});
}
Within the component has a method that is called when a change url.
beforeRouteEnter (to, from, next) {
next(vm =>{
Auth.check(vm);
if (!Auth.user.authenticated) {
next({path:'/login'});
}
})
}
Function next() given Vue app instance, then check user object. If user false, next() call again for redirect to login page. All it works, but only when the page is already loaded. If i'll reload /account page, there is a redirect to /login page, because request to Api not completed yet, but code will continue execute. Any idea?
Quite simple to do, you need to make your code work asynchronously and hold routing before request is completed.
export default {
user: {
authenticated : false
},
check: function(context) {
return context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) => {
console.log(response)
});
}
}
then
beforeRouteEnter (to, from, next) {
next(vm => {
Auth.check(vm).then(() => {
if (!Auth.user.authenticated) {
next({path:'/login'});
} else {
next()
}
}
})
}
Other pro tips
Display some loading indicator when loading so your application doesn't seem to freeze (you can use global router hooks for that)
If you are using vue-resource, consider using interceptors (perhaps in addition to the routing checks)
Consider using router.beforeEach so that you don't have to copy-paste beforeRouteEnter to every component
Done. Need to return promise like that
check: function(context) {
return context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) =>{
console.log(response)
});
}
and then
beforeRouteEnter (to, from, next) {
Auth.check().then(()=>{
if(!Auth.user.authenticated)
next({path:'/login'})
else
next();
})
}

Resources