getRouter: function () {
return UIComponent.getRouterFor(this);
},
onInit: function () {
firebase.auth().onAuthStateChanged((user) => {
if (!user) {
this.getRouter().navTo("login");
}
});
}
Is there a way to make my routes protected, it is no problem when the first time I type in browser, I got redirected, but if I logout from the component, and manually type in browser /admin, I am not redirected, so onInit works only one time. I am new to sapui5, and in React it is different. Can someone please help and explain if there is a way to make some kind of protection on routes. Thanks.
The correct event to listen for is patternMatched
This is an example of how to protect a single route called "myProtectedRoute":
onInit: function () {
this.getRouter().getRoute("myProtectedRoute").attachPatternMatched((event) => {
if (user.isAuthenticated) {
// load and show data
} else {
this.getRouter().navTo("login");
}
})
}
If you wish to be notified when any route is matched you can read more in this tutorial https://openui5.hana.ondemand.com/topic/4a063b8250f24d0cbf7c689821df7199
Related
In Vue component I have some data that comes from a localStorage.
if (localStorage.getItem("user") !== null) {
const obj_user = localStorage.getItem('user');
var user = JSON.parse(obj_user);
} else {
user = null;
}
return {
user
}
After in this component I have to check this data. I do it like this
<li v-if="!user">Login</li>
<li v-if="user">
<span>{{user.name}}</span>
</li>
But the data does not change immediately, but only after the page is reloaded. Right after I login in on the page and redirected the user to another page, I still see the Login link.
What am I doing wrong?
Maybe there is some other way how to check and output data from the localstorage in component?
Thank you in advance.
It looks like the issue may be related to they way that your user variable is not a piece of reactive stateful data. There isn't quite enough code in your question for us to determine that for sure, but it looks like you are close to grasping the right way to do it in Vue.
I think my solution would be something like this...
export default {
data() {
return {
user: null,
}
},
methods: {
loadUser() {
let user = null;
if (localStorage.getItem("user")) {
const obj_user = localStorage.getItem("user");
user = JSON.parse(obj_user);
}
this.user = user;
}
},
created() {
this.loadUser();
}
}
I save the user in the data area so that the template will react to it when the value changes.
The loadUser method is separated so that it can be called from different places, like from a user login event, however I'm making sure to call it from the component's created event.
In reality, I would tend to put the user into a Vuex store and the loadUser method in an action so that it could be more globally available.
I'm having difficulties understanding how to best implement Firebase Auth in a SPA web application. I'm new to both SPAs and Firebase.
My app consists of both secure pages and non-secure pages. The non-secure pages are for things like terms & conditions, privacy policy and forgot password.
Inside my app code, at the highest level e.g. /app.js, I'm importing a Firebase Auth configuration module as the first order of operation. This module contains the following function which listens for changes in authentication and acts accordingly.
firebase.auth().onAuthStateChanged(user => {
if (!user) {
Store.router.navigate("/login"); // <-- this is my problem
} else {
// get user data from Cloud Firestore
// store user data locally
}
});
This is my router at it's basic level:
router.on({
'/': () => {
// import module
},
'/login': () => {
// import module
},
'/forgot-password': () => {
// import module
}
}).resolve();
Before I decided to use Firebase Auth, my router checked for authentication at each route and looked a little like this:
router.on({
'/': () => {
if (isAuthenticated) {
// import module
} else {
router.navigate("/login")
}
},
'/login': () => {
if (!isAuthenticated) {
// import module
} else {
router.navigate("/")
}
},
'/forgot-password': () => {
// import module
}
}).resolve();
Every time a route changes using the Firebase Auth version of my app, the onAuthStateChanged listener receives an update and, if the user is logged out, it redirects them to the /login page. If logged in, it grabs the user's full profile from the database and stores it locally.
Now, this works brilliantly unless the user is logged out, is on the /login page, and wants to visit the /forgot-password page. When a user navigates to this page, or any other no-secure, public page, the authentication listener updates itself and redirects the user back to /login instantly and this is wrong.
This is highly undesirable but I really like the way this listener works other than that, as if/when a user has multiple tabs open and logs out of one, it returns all tabs back to /login.
How can I configure this listener, or reconfigure my app, to allow the public pages to be available too? And should I be unsubscribing from the listener?
I managed to solve the problem, so I'll share my findings here for others. I did however lose the functionality that returned all open tabs to the login page when they logged out of one but this does work better for my app that has public routes.
I now have a method in my User module called getCurrentUser() which is now where the onAuthStateChanged observable sits. Because I used the 'unsubscribe()` method, I can now call this as and when I need it without having it observing continuously.
getCurrentUser: () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
unsubscribe();
resolve(user);
}, reject);
})
}
In my router, I can now check the auth state by calling and waiting for User.getCurrentMethod().
router.on({
'/': async () => {
if (await User.getCurrentUser()) {
// import module
// load HTML
} else {
router.navigate('/login')
}
},
'/login': () => {
...
}
I am creating a react app - using create-react-app and amplify - and I am trying to set up authentication. I don't seem to be able to handle the federated logins using the hosted UI.
There are some pages which require no authentication to reach and then some which require a user to be logged in. I would like to use the hosted UI since that's prebuilt. I have been following the getting started docs here: https://aws-amplify.github.io/docs/js/authentication
For background I have the following components:
- Amplify - an amplify client which wraps calls in methods like doSignIn doSignOut etc. The idea is to keep all this code in one place. This is a plain javascript class
- Session - provides an authentication context as a React context. This context is set using the amplify client. It has HOC's for using the context
- Pages - some wrapped in the session HOC withAuthentication which only renders the page if the user has logged in
This structure is actually taken from a Firebase tutorial: https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/
Maybe this is just not feasible with Amplify? Though the seem similar enough to me that it should work. The basic idea is that the Session provides a single auth context which can be subscribed to by using the withAuthentication HOC. That way any component that requires a user will be rendered as soon as a user has logged in.
Originally I wrapped the entire App component in the withAuthenticator HOC provided by amplify as described in the docs. However this means that no pages are accessible without being authenticated - home page needs to be accessible without an account.
Next I tried calling to the hosted UI with a sign in button and then handling the response. The problem is when the hosted UI has logged a user in then it redirects back to the app causing it to reload - which is not ideal for a single page app.
Then I tried checking if the user is authenticated every time the app starts - to deal with the redirect - but this becomes messy as I need to move a lot of the amplify client code to the Session context so that it can initialise correctly. The only way I can see to get this is using the Hub module: https://aws-amplify.github.io/docs/js/hub#listening-authentication-events The downside is that after logging in, the app refreshes and there's still a moment when you are logged out which makes the user experience weird.
I would have thought that there would be a way to not cause an application refresh. Maybe that's just not possible with the hosted UI. The confusing thing to me is that the documentation doesn't mention it anywhere. In actual fact there is documentation around handling the callback from the hosted UI which as far as I can see never happens because the entire page refreshes and so the callback can never run.
I've tried to trim this down to just what's needed. I can provide more on request.
Amplify:
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../aws-exports';
import { AuthUserContext } from '../Session';
class AmplifyClient {
constructor() {
Amplify.configure(awsconfig);
this.authUserChangeListeners = [];
}
authUserChangeHandler(listener) {
this.authUserChangeListeners.push(listener);
}
doSignIn() {
Auth.federatedSignIn()
.then(user => {
this.authUserChangeListeners.forEach(listener => listener(user))
})
}
doSignOut() {
Auth.signOut()
.then(() => {
this.authUserChangeListeners.forEach(listener => listener(null))
});
}
}
const withAmplify = Component => props => (
<AmplifyContext.Consumer>
{amplifyClient => <Component {...props} amplifyClient={amplifyClient} />}
</AmplifyContext.Consumer>
);
Session:
const provideAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
componentDidMount() {
this.props.amplifyClient.authUserChangeHandler((user) => {
this.setState({authUser: user});
});
}
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withAmplify(WithAuthentication);
};
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
render() {
return (
<AuthUserContext.Consumer>
{user =>
!!user ? <Component {...this.props} /> : <h2>You must log in</h2>
}
</AuthUserContext.Consumer>
);
}
}
return withAmplify(WithAuthentication);
};
The auth context is provided once at the top level:
export default provideAuthentication(App);
Then pages that require authentication can consume it:
export default withAuthentication(MyPage);
What I would like to happen is that after the user signs in then I can set the AuthUserContext which in turn updates all the listeners. But due to the redirect causing the whole app to refresh the promise from Auth.federatedSignIn() can't resolve. This causes the user to be displayed with You must log in even though they just did.
Is there a way to block this redirect whilst still using the hosted UI? Maybe launch it in another tab or in a popup which doesn't close my app? Or am I going about this the wrong way? It just doesn't feel very 'Reacty' to cause full page refreshes.
Any help will be greatly appreciated. I can provide more details on request.
Instead of chaining onto the Auth's promise, you can use Amplify's build-in messaging system to listen to events. Here is how I do it in a custom hook and how I handle what gets rendered in Redux.
import { Auth, Hub } from 'aws-amplify';
import { useEffect } from 'react';
function useAuth({ setUser, clearUser, fetchQuestions, stopLoading }) {
useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
if (event === 'signIn') {
setUser(data);
fetchQuestions();
stopLoading();
}
if (event === 'signOut') {
clearUser();
stopLoading();
}
});
checkUser({ fetchQuestions, setUser, stopLoading });
}, [clearUser, fetchQuestions, setUser, stopLoading]);
}
async function checkUser({ fetchQuestions, setUser, stopLoading }) {
try {
const user = await Auth.currentAuthenticatedUser();
setUser(user);
fetchQuestions();
} catch (error) {
console.log(error);
} finally {
stopLoading();
}
}
How the route is changed, matters for my case.
So, I want to catch when the route is changed by a back button of browser or gsm.
This is what I have:
router.beforeEach((to, from, next) => {
if ( /* IsItABackButton && */ from.meta.someLogica) {
next(false)
return ''
}
next()
})
Is there some built-in solutions that I can use instead of IsItABackButton comment? Vue-router itself hasn't I guess but any workaround could also work here. Or would there be another way preferred to recognize it?
This is the only way that I've found:
We can listen for popstate, save it in a variable, and then check that variable
// This listener will execute before router.beforeEach only if registered
// before vue-router is registered with Vue.use(VueRouter)
window.popStateDetected = false
window.addEventListener('popstate', () => {
window.popStateDetected = true
})
router.beforeEach((to, from, next) => {
const IsItABackButton = window.popStateDetected
window.popStateDetected = false
if (IsItABackButton && from.meta.someLogica) {
next(false)
return ''
}
next()
})
Slight improvement to #yair-levy answer.
Wrapping push to own navigate method is not convenient because you usually want to call push() from various places. Instead, router original methods can be patched in one place without changes in remaining code.
Following code is my Nuxt plugin to prevent navigation triggered by back/forward buttons (used in Electron app to avoid back caused by mouse additional "back" button, which makes mess in Electron app)
Same principle can be used for vanilla Vue and to track common back button together with your custom handling.
export default ({ app }, inject) => {
// this is Nuxt stuff, in vanilla Vue use just your router intances
const { router } = app
let programmatic = false
;(['push', 'replace', 'go', 'back', 'forward']).forEach(methodName => {
const method = router[methodName]
router[methodName] = (...args) => {
programmatic = true
method.apply(router, args)
}
})
router.beforeEach((to, from, next) => {
// name is null for initial load or page reload
if (from.name === null || programmatic) {
// triggered bu router.push/go/... call
// route as usual
next()
} else {
// triggered by user back/forward
// do not route
next(false)
}
programmatic = false // clear flag
})
}
As stated by #Yuci, all the router hook callbacks are performed before popstate is updated (and therefore not helpful for this use case)
What you can do:
methods: {
navigate(location) {
this.internalNavigation = true;
this.$router.push(location, function () {
this.internalNavigation = false;
}.bind(this));
}
}
Wrap 'router.push' with you own 'navigate' function
Before calling router.push, set 'internalNavigation' flag to true
Use vue router 'oncomplete' callback to set internalNavigation flag back to false
Now you can check the flag from within beforeEach callback and handle it accordingly.
router.beforeEach((to, from, next) => {
if ( this.internalNavigation ) {
//Do your stufff
}
next()
})
I found a simple way to solve this after spending a lot of time trying to refine the codes to work well in my case and without a glitch.
export const handleBackButton = () => {
router.beforeEach((to, from, next) => {
if (window.event.type == 'popstate' && from.name == 'HomePage'){
next(false);
}else{
next();
}
});
}
The window.event.type == 'popstate' checks if the back button is pressed
And from.name == 'HomePage' checks the page on which the back button is pressed or you are routing from.
HomePage as the name where you want to disable back button. You can leave this condition if you want to disable it throughout the site.
next(false) and next() to stop or allow navigation respectively.
You can place the code in a navigationGuard.js file and import it to your main.js file
I tried other methods, including calling from the components but it produces a glitch and the rerouting becomes obvious. But this leaves no glitch at all.
Hope this works for you. Cheers
I had the same problem regarding detecting Back Button navigation as opposed to other types of navigation in my Vue App.
What I ended up doing was adding a hash to my real internal App navigation to differentiate between intended App navigation and Back Button navigation.
For example, on this route /page1 I want to catch Back Button navigations to close models that are open. Imagine I really wanted to navigate to another route, I'll add a hash to that route: /page2#force
beforeRouteLeave(to, from, next) {
// if no hash then handle back button
if (!to.hash) {
handleBackButton();
next(false); // this stops the navigation
return;
}
next(); // otherwise navigate
}
This is rather simplistic, but it works. You'll want to check what the hash actually contains if you use them for more than this in your app.
performance.navigation is deprecated so whatch out! https://developer.mozilla.org/en-US/docs/Web/API/Performance/navigation
When you want to register any global event listener you should be very careful with that. It will be called each time since registration moment untill you unregister that manualy. For me the case was that I have register popstate listener when component was created to listen and call some action when:
browser back button
alt + arrow back
back button in mouse
was clicked. After that I have unregister popstate listener to not call it in other components where I don't want it to be called, keep Your code and method calls clean :).
My code sample:
created() {
window.addEventListener('popstate', this.popstateEventAction );
},
methods: {
popstateEventAction() {
// ... some action triggered when the back button is clicked
this.removePopstateEventAction();
},
removePopstateEventAction() {
window.removeEventListener('popstate', this.popstateEventAction);
}
}
Best regards!
The accepted answer almost worked for me, but I found that the listener was behind by 1 click, probably due to the issue that #Yuci highlighted.
The answer from #farincz worked best for me, but since it wasn't written for vanilla Vue, I thought I'd write down what worked for me here:
// after createRouter
let programmatic = false;
(['push', 'replace', 'go', 'back', 'forward']).forEach(methodName => {
const method = router[methodName]
router[methodName] = (...args) => {
programmatic = true
method.apply(router, args)
}
})
router.beforeEach(async (to, from) => {
if(!from.name === null || !programmatic) {
// do stuff you want to do when hitting back/forward or reloading page
}
programmatic = false // clear flag
});
This is done very easily.
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
// History back position, if user click Back button
return savedPosition
} else {
// Scroll to top of page if the user didn't press the back button
return { x: 0, y: 0 }
}
}
})
Check here:
https://router.vuejs.org/guide/advanced/scroll-behavior.html#async-scrolling
does anyone know how to check a user's permissions before a vue.js route is rendered? I came up with a partial solution by checking permissions in the created stage of a component:
created: function () {
var self = this;
checkPermissions(function (result) {
if(result === 'allowed') {
// start making AJAX requests to return data
console.log('permission allowed!');
} else {
console.log('permission denied!');
self.$router.go('/denied');
}
});
}
However, the issue is the entire page loads momentarily (albeit without any data) before the checkPermission() function gets activated and re-routes to /denied.
I also tried adding the same code in the beforeCreate() hook, but it didnt seem to have any effect.
Does anyone else have any other ideas? Note - the permissions vary from page to page.
Thanks in advance!
The thing you need is defined in Vue Router Docs as Data Fetching - https://router.vuejs.org/en/advanced/data-fetching.html
So, as docs says, sometimes you need to fetch data when route is activated.
Keep checkPermissions in created hook, and then watch the route object:
watch: {
// call again the method if the route changes
'$route': 'checkPermissions'
}
A maybe more handy solution would be move logic from created hook into separated method and then call it in hook and in watch object too, as well.(Using ES6 syntax bellow)
export default {
created() {
this.checkPermissions()
},
watch: {
'$route': 'checkPermissions'
},
methods: {
checkPermissions() {
// logic here
}
}
}