I have a function to authorize the user, so the user has restrictions on certain components. The function works well with the 'beforeEnter' navigation-guard. How to use/write the same function in the router beforeEach router-guard?
I tried writing the function in beforeEach but the javascript functions (forEach, includes,..) are not working in the router.beforeEach.
This is the function to authorize
function authorizeUser(to) {
const currentUser = store.getters["auth/isCurrentUser"];
console.log(currentUser);
currentUser.teams.forEach((team) => {
const validPermissions = team.permissions.filter((item) => { return to.meta.neededPermissions.includes(item.permissionType); }); //returns array of objects
const mappedValidPermissions = validPermissions.map((item) => { return item.permissionType; });// returns array with permissionType
// returned matched permissions
console.log(
JSON.stringify(to.meta.neededPermissions),
JSON.stringify(mappedValidPermissions),
);
if (!to.meta.neededPermissions.every(i=>mappedValidPermissions.includes(i))) {
router.push({ path: "/:notFound(.*)" });
}
});
}
This is the router.beforeEach nav-guard-
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.getters["auth/isLoggedIn"]) {
next({
name: "Authentication",
params: {
desiredRoute: to.fullPath,
},
});
} else {
next();
}
});
How to utilize the above function and if condition in beforeEach, so that I can check each router link before giving access?
What version of vue-router are you using. The current version's signature of beforeEach changed. Instead of calling next, you would optionally return either false or a route location object.
Related
I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.
I am trying to load a notification token (notificationToken) that I've stored within Firebase to a React Native component.
Once the notificationToken is loaded to my redux state, I want to check for my device permissions to see if the notificationToken has expired within the function getExistingPermission() that I run in the componentDidMount().
If the token has expired, then I'll replace the token within Firebase with the new token. If it's the same, then nothing happens (which is intended functionality).
When I'm running my function getExistingPermission() to check if the token is up-to-date the Firebase listener that pulls the notificationToken does not load in time, and so it's always doing a write to the Firebase database with a 'new' token.
I'm pretty sure using async/await would solve for this, but have not been able to get it to work. Any idea how I can ensure that the notificationToken loads from firebase to my redux state first before I run any functions within my componentDidMount() function? Code below - thank you!
src/screens/Dashboard.js
Should I use a .then() or async/await operator to ensure the notificationToken loads prior to running it through the getExistingPermission() function?
import {
getExistingPermission
} from '../components/Notifications/NotificationFunctions';
componentDidMount = async () => {
// Listener that loads the user, reminders, contacts, and notification data
this.unsubscribeCurrentUserListener = currentUserListener((snapshot) => {
try {
this.props.watchUserData();
} catch (e) {
this.setState({ error: e, });
}
});
if (
!getExistingPermission(
this.props.notificationToken, //this doesn't load in time
this.props.user.uid)
) {
this.setState({ showNotificationsModal: true });
}
};
src/components/Notifications/NotificationFunctions.js
The problem is probably not here
export const getExistingPermission = async (
notificationToken,
uid,
) => {
const { status: existingStatus } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
if (existingStatus !== 'granted') {
console.log('status not granted');
return false;
} else {
let token = await Notifications.getExpoPushTokenAsync();
/* compare to the firebase token; if it's the same, do nothing,
if it's different, replace */
if (token === notificationToken) {
console.log('existing token loaded');
return true;
} else {
console.log('token: ' + token);
console.log('notificationToken: ' + notificationToken);
console.log('token is not loading, re-writing token to firebase');
writeNotificationToken(uid, token);
return false;
}
}
};
src/actions/actions.js
// Permissions stuff
watchPermissions = (uid) => (
(dispatch) => {
getPermissions(uid + '/notificationToken', (snapshot) => {
try {
dispatch(loadNotificationToken(Object.values([snapshot.val()])[0]));
}
catch (error) {
dispatch(loadNotificationToken(''));
// I could call a modal here so this can be raised at any point of the flow
}
});
}
);
// User Stuff
export const watchUserData = () => (
(dispatch) => {
currentUserListener((user) => {
if (user !== null) {
console.log('from action creator: ' + user.displayName);
dispatch(loadUser(user));
dispatch(watchReminderData(user.uid)); //listener to pull reminder data
dispatch(watchContactData(user.uid)); //listener to pull contact data
dispatch(watchPermissions(user.uid)); //listener to pull notificationToken
} else {
console.log('from action creator: ' + user);
dispatch(removeUser(user));
dispatch(logOutUser(false));
dispatch(NavigationActions.navigate({ routeName: 'Login' }));
}
});
}
);
export const loadNotificationToken = (notificationToken) => (
{
type: 'LOAD_NOTIFICATION_TOKEN',
notificationToken,
}
);
Tony gave me the answer. Needed to move the permissions check to componentDidUpdate(). For those having a similar issue, the component looks like this:
src/screens/Dashboard.js
componentDidUpdate = (prevProps) => {
if (!prevProps.notificationToken && this.props.notificationToken) {
if (!getExistingPermission(
this.props.notificationToken,
this.props.user.uid
)) {
this.setState({ showNotificationsModal: true });
}
}
};
Take a look at redux subscribers for this: https://redux.js.org/api-reference/store#subscribe . I implement a subscriber to manage a small state machine like STATE1_DO_THIS, STATE2_THEN_DO_THAT and store that state in redux and use it to render your component. Only the subscriber should change those states. That gives you a nice way to handle tricky flows where you want to wait on action1 finishing before doing action2. Does this help?
In my route file I have a condition where an external function checks if two values are equal, obj.id === getId and accordingly to true or false it renders the view or it blocks the user.
The function checkUser which I try to work as a callback, it gets the values which are being pass to it but I cannot get back the result of true or false to my route file.
Route file
const express = require('express')
const router = express.Router()
const fcs = require('../routes/functions') //Global functions file
router.get('/:id', fcs.isLoggedIn, function(req, res, next) {
getId = req.params.id
db.con.query('SELECT * FROM employees where id=?', getId, function(err, results, callback) {
if (err) {
//some code
return
} else {
try {
//some code for getting results to obj{}
// here is the checking point that will use the callback
if (fcs.checkUser(obj.id, getId)) {
res.render('showuser')
} else {
//some code
}
} catch (err) {
//some code
}
}
})
})
Global Functions file
const express = require('express')
const router = express.Router()
module.exports.checkUser = function checkUser(uid, id, callback) {
// it gets true or false with no problem
console.log(uid === id)
// Need to send the output of true or false back to the condition checker (route file)
callback(uid === id)
}
You need to pass another callback.
For Your callback implementation it should probably look like this:
// here is the checking point that will use the callback
fcs.checkUser(obj.id, getId, function(userValid){
if(userValid){
res.render('showuser');
} else {
// some code
}
));
Node-style callback:
// here is the checking point that will use the callback
fcs.checkUser(obj.id, getId, function(err, userValid){
if(userValid){
res.render('showuser');
} else {
// some code
}
));
Than your checkUser should call callback like this: callback(null, uid === id);
I'm having this problem with Vue Router.
My scenario is to allow / to be loaded first, fetch some data from there and then it will go to /chat/:chatId route and render UI there, where :chatId param is set from the data I got from API.
I have / and ['chat', 'chat/:chatId'] as route alias.
I have in-component guard for this matter.
beforeRouteEnter(to, from, next) {
store.dispatch('fetchOpenSessions')
.then(() => {
const firstChat = Object.keys(store.state.chatsidebar.openSessions)[0];
next({ name: 'chat', params: { chatId: firstChat } });
});
},
However this code ends up looping infinitely, causing the browser to hang.
My question is, how do I set chat/:chatId if my initial route is / or /chat without getting into infinite loop?
beforeRouteEnter(to, from, next) {
store.dispatch('fetchOpenSessions')
.then(() => {
if(to.name == 'chat'){
next();
}else{
const firstChat = Object.keys(store.state.chatsidebar.openSessions)[0];
next({ name: 'chat', params: { chatId: firstChat } });
}
});
},
I would like to do some unit testing (mocha/chai) for my meteor application. I'm using validated methods (which shouldn't matter for this).
In my method I'm checking if the user has admin permission to perform a collection update.
How do I set a 'dummy' in my unit test, as right now the test will always fail with a 403 error throw.
Unit test
describe('method', () => {
it('should update document', (done) => {
articleUpdate.call({ _id, value })
}
})
Method
const articleUpdate = new ValidatedMethod({
name: 'article.update',
validate: null,
run ({ _id, value }) {
const loggedInUser = Meteor.user()
const isAdmin = Roles.userIsInRole(loggedInUser, ['group'], 'admin')
if (!isAdmin) { throw new Meteor.Error(403, 'Access denied') }
Articles.update(_id, {
$set: { content: value }
})
}
})
In test mode you can use _execute to execute a validated method to pass a context, see here. However, the easiest thing here seems to stub Roles.userIsInRole like so:
import { sandbox } from 'sinon';
const sb = sandbox.create();
describe('method', () => {
it('should update document', () => {
sb.stub(Roles, 'userIsInRole').callsFake(() => true);
articleUpdate.call({ _id, value })
}
})