We're switching from SPA to statically generated, and are running into a problem with middleware.
Basically, when Nuxt is statically rendered, middleware is run on the build server first, and then is run after each page navigation client side. The important point is that middleware is not run client side on first page load. This is discussed here
We work around this for some use cases by creating a plugin that uses the same code, since plugins are run on the first client load.
However, this pattern doesn't work well for this use case. The following is an example of the middleware that we want to use:
// middleware/authenticated.js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
// Inside a component
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
This example is taken directly from the Nuxt docs.
When rendered statically, this middleware is not called on first page load, so a user might end up hitting their dashboard before they've logged in, which causes problems.
To add this to a plugin, the only way I can think to do this is by adding a list of authenticated_routes, which the plugin could compare to and see if the user needs to be authed.
The problem with that solution though is that we'd then need to maintain a relatively complex list of authed pages, and it's made worse by having dynamic routes, which you'd need to match a regex to.
So my question is: How can we run our authenticated middleware, which is page specific, without needing to maintain some list of routes that need to be authenticated? Is there a way to actually get the middleware associated to a route inside a plugin?
To me it is not clear how to solve it the right way. We are just using the static site generation approach. We are not able to run a nuxt middleware for the moment. If we detect further issues with the following approach we have to switch.
One challenge is to login the user on hot reload for protected and unprotected routes. As well as checking the login state when the user switches the tabs. Maybe session has expired while he was on another tab.
We are using two plugins for that. Please, let me know what you think.
authRouteBeforeEnter.js
The plugin handles the initial page load for protected routes and checks if the user can access a specific route while navigating around.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes"
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
if(to.name === 'logout'){
await store.dispatch('app/shutdown', {userLogout:true})
return next('/')
}
if(PROTECTED_ROUTES.includes(to.name)){
if(document.cookie.indexOf('PHPSESSID') === -1){
await store.dispatch('app/shutdown')
}
if(!store.getters['user/isLoggedIn']){
await store.dispatch('user/isAuthenticated', {msg: 'from before enter plugin'})
console.log('user is logged 2nd try: ' + store.getters['user/isLoggedIn'])
return next()
}
else {
/**
* All fine, let him enter
*/
return next()
}
}
return next()
})
}
authRouterReady.js
This plugin ment for auto login the user on unprotected routes on initial page load dnd check if there is another authRequest required to the backend.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes";
export default function ({ app, store }) {
app.router.onReady(async (route) => {
if(PROTECTED_ROUTES.includes(route.name)){
// Let authRouterBeforeEnter.js do the job
// to avoid two isAuthorized requests to the backend
await store.dispatch('app/createVisibilityChangedEvent')
}
else {
// If this route is public do the full init process
await store.dispatch('app/init')
}
})
}
Additionally i have added an app module to the store. It does a full init process with auth request and adding a visibility changed event or just adds the event.
export default {
async init({ dispatch }) {
dispatch('user/isAuthenticated', {}, {root:true})
dispatch('createVisibilityChangedEvent')
},
async shutdown({ dispatch }, {userLogout}) {
dispatch('user/logout', {userLogout}, {root:true})
},
async createVisibilityChangedEvent({ dispatch }) {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
console.log('visible changed');
await dispatch('user/isAuthenticated', {}, {root:true})
}
})
},
}
Related
I am doing user authentication, but I ran into a problem. First, when loading, the middleware is loaded, it does not see the authorized user through $fire.auth.onAuthStateChanged. And if you go to another page (without reloading page), user is appear. How to make him see it at the first boot?
Here is my middleware
export default function ({app, route, redirect}) {
console.log('middleware')
app.$fire.auth.onAuthStateChanged(user => {
if (user) {
console.log('user+')
if (route.path === perm.signin || route.path === perm.signup) {
return redirect('/')
}
} else {
console.log('user-')
if (route.path !== perm.signin || route.path !== perm.signup) {
return redirect(perm.signin)
}
}
})
}
and what I received (1st pic) when first enter to app in console.log(middleware, user-). But if I go to another page I receive middleware, user+.
I need user + to be on the first start
onAuthStateChanged fires the callback you provided as argument after the middleware has run.
In other words the callback's return statements are not being run when the middleware runs.
You could either ensure the middleware is called after the first authentication attempt, but this would slow down the initial startup of the application. So you could expose the nuxt router to the onAuthStateChanged handler and router.push('/login') or router.push('/somewhere') from there.
Specifically, code that runs before the app actually loads. I'm using vuex and the first thing I want to do (regardless of what route the user is on) is to dispatch a getUser action to get currently user details from the API (or alternatively, redirect if not authenticated).
If I place it in my App.vue mounted component, I believe it might be too late? Don't children components load before parents?
If I get it right you want to do something before the application initialize. For that you can just perform async method in app initialization. Something like that as an example:
function initializeApp (vueCreated) {
return new Promise((resolve, reject) => {
switch (vueCreated) {
case false: // "prevue" initialization steps
console.log('vue not yet created, prevue steps happens')
// ...
setTimeout(_ => resolve(), 3500) // async call
break;
case true: // we can continue/prepare data for Vue
console.log('vue created, but waiting for next initialization steps and data')
// ...
setTimeout(_ => resolve('Mounted / shown when app ready'), 3500) // async call
}
})
}
initializeApp(false).then(_ => {
new Vue({
template: '#app',
data: {
content: null
},
async created () {
this.content = await initializeApp(true)
this.$mount('#app')
console.log('all inicialization steps done, data arrived, vue mounted')
}
})
})
I have found some article related to your question may be this help you out. Link
If you are using vue-router you can use beforeEach to prevent some routes of unauthenticated users.
You can read more here.
If you get stuck here provide code what you tried with router.
Also good example of using navigation guards.
Good luck!
I have a bit of a pickle.
I am using Route guard (implementing CanActivate interface) to check if user is granted access to particular route:
const routes: Routes = [
{
path: '',
component: DashboardViewComponent
},
{
path: 'login',
component: LoginViewComponent
},
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']},
canActivate: [RouteGuard]
},
{
path: '**',
component: ErrorNotFoundViewComponent
}
];
Now it works great in protecting the '/protected/foo' route from activating, but I would like to tell the user that route he is trying to access is forbidden (similar to 403 Forbidden you may get from server).
The problem:
How do I show the user this special error view without redirecting him to error route which seams to be the preferred option by so many sources I have found?
And how do I still use my RouteGuard without actually loading the forbidden route, because if I check access inside my FooViewComponent and display different view it kind of defeats point of having RouteGuard in the first place.
Ideally I would like to have my RouteGuard not only returning false in canActivate() method, but also replace component completely with say ErrorForbiddenViewComponent. But I have no idea how to do it, or is it event possible. Any alternatives?
This is how my route guard looks now:
import {Injectable} from '#angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '#angular/router';
import {AuthService} from '../services/auth.service';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(
private router: Router,
private auth: AuthService
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const { auth, router } = this;
const { allowAccessTo } = next.data;
const identity = auth.getIdentity();
if (
identity &&
allowAccessTo.indexOf(identity.role)
) {
// all good, proceed with activating route
return true;
}
if (identity) {
// TODO show ErrorForbiddenViewComponent instead of redirecting
console.log('403 Forbidden >>', next);
}
else {
// not logged in: redirect to login page with the return url
const [returnUrl, returnQueryParams] = state.url.split('?');
console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
}
return false;
}
}
So I am just preventing route from loading, but I am not redirecting. I only redirect non logged visitors to login route.
Reasoning:
Routes should reflect certain state of application - visiting a route
url should recreate that state
To have error routes (except for 404 Not Found) would mean your application can actually recreate error states. This makes no sense
as why would you keep error state as state of your application? For
debugging purpose one should use logs (console or server), revisiting
error page (i.e. page refresh) might interfere with that.
Also by redirecting to error route app should provide some insights of error to user. For that matter either some parameter would need to
be passed via url or (far worse) keeping the error sate in some error
service and retrieve it upon accessing error route.
Also, ignoring the RouteGuard and just loading the component and checking access inside it may result in some extra dependencies
loaded which would not be used anyway (as user is not allowed), makes
the whole lazy loading much harder.
Does anyone have some kind of solution for this? I also wonder how come that after Angular 2+ being around for so long nobody had this kind of situation before? Everybody is just ok with redirecting?
Also keep in mind that although I am currently using the FooViewComponent synchronously, that may change in future!
I had once worked on the similar problem.
Sharing my stackblitz poc where I have created -
Authenticated Component (with guard)
Login Component
Permission Guard
Route (/auth route is provided with PermissionGuardService guard)
The guard is evaluating the user type and handling the redirection / error accordingly.
The use cases are -
User is not logged in (shows a toast with log in message)
User is not admin (shows a toast with unauthorised message)
User is admin (show a toast with success messaage)
I have stored the user in local storage.
EDIT - DEMO
Let me know if you need a special handling in it and I will update the code base.
Cheers!
After looking at angular2 example provided by Tarun Lalwani in comments of question and after taking deeper look into Dynamic component loader article on Angular docs I have managed to apply it to my code:
I no longer use my RouteGuard when specifying routes:
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']}, // admin only
canActivate: [RouteGuard]
},
Instead I have created special RouteGuardComponent and here is how I use it:
{
path: 'protected/foo',
component: RouteGuardComponent,
data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},
This is the code of RouteGuardComponent:
#Component({
selector: 'app-route-guard',
template: '<ng-template route-guard-bind-component></ng-template>
// note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {
#ViewChild(RouteGuardBindComponentDirective)
bindComponent: RouteGuardBindComponentDirective;
// ^^ and here we bind to that directive instance in template
constructor(
private auth: AuthService,
private route: ActivatedRoute,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit() {
const {auth, route, componentFactoryResolver, bindComponent} = this;
const {component, allowAccessTo} = route.snapshot.data;
const identity = auth.getIdentity();
const hasAccess = identity && allowAccessTo.indexOf(identity.role);
const componentFactory = componentFactoryResolver.resolveComponentFactory(
hasAccess ?
component : // render component
ErrorForbiddenViewComponent // render Forbidden view
);
// finally use factory to create proper component
routeGuardBindComponentDirective
.viewContainerRef
.createComponent(componentFactory);
}
}
Also, this requires special directive to be defined (I am sure this can be done some other way, but I have just applied that Dynamic component example from Angular docs):
#Directive({
selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
It isn't full answer to my own question (but its a start), so if somebody provides something better (i.e. a way to still use canActivate and ability to lazy load) I'll make sure to take that into account.
Your RouteGuard can inject whatever service you're using for modal windows, and the .canActivate() can pop the modal without redirection to inform the user without disturbing the current state of the app.
We use toastr and its angular wrapper for this, since it creates a modeless pop-up that self-dismisses after so-many seconds, no OK/Cancel buttons needed.
I've recently come across the same problem. In the end, I couldn't manage to do this using CanActivate guard, so I've implemented the authorisation logic in the component that holds the <router-outlet>.
Here is its template:
<div class="content">
<router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
<div class="message">
<mat-icon>lock</mat-icon>
<span>Access denied.</span>
</div>
</ng-template>
And its source code:
import { ActivatedRoute, ActivationStart, Router } from '#angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '#angular/core';
#Component({
selector: 'app-panel-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {
/**
* A stream of flags whether access to current route is permitted.
*/
accessAllowed$: Observable<boolean>;
constructor(
permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
route: ActivatedRoute,
router: Router,
) {
const streams: Observable<boolean>[] = [];
/*
The main purpose of this component is to replace `<router-outlet>` with "Access denied"
message, if necessary. Such logic will be universal for all possible route components, and
doesn't require any additional components - you will always have at least one component with
`<router-outlet>`.
This component contains `<router-outlet>`, which by definition means that all possible authorisable
routes are beneath it in the hierarchy.
This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route
itself in this component will always be the parent route of the one we need to process.
So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
router events.
However, by the time an instance of this component is constructed, all routing events will have been
triggered. This is especially important in case user loads the page on this route.
To solve that, we can merge two streams, the first one of which will be a single access flag
for **activated route**, and the second will be a stream of flags, emitted from router
events (e.g. caused by user navigating through app).
This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the
last value emitted from the stream created from router events will be `true`.
*/
const deepestChild = this.findDeepestTreeNode(route);
const currentData = deepestChild.routeConfig.data;
// `data.authActions` is just an array of strings in my case
if (currentData &&
currentData.authActions &&
Array.isArray(currentData.authActions) &&
currentData.authActions.length > 0) {
streams.push(
// `hasPermissions(actions: strings[]): Observable<boolean>`
permissions.hasPermissions(currentData.authActions).pipe(take(1))
);
} else {
// If the route in question doesn't have any authorisation logic, simply allow access
streams.push(of(true));
}
streams.push(router.events
.pipe(
filter(e => e instanceof ActivationStart),
switchMap((event: ActivationStart) => {
const data = event.snapshot.data;
if (data.authActions &&
Array.isArray(currentData.authActions) &&
data.authActions.length > 0) {
return permissions.hasPermissions(data.authActions);
}
return of(true);
}),
));
this.accessAllowed$ = merge(...streams);
}
/**
* Returns the deepest node in a tree with specified root node, or the first
* encountered node if there are several on the lowest level.
*
* #param root The root node.
*/
findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
const findDeepest = (node: T, level = 1): [number, T] => {
if (node.children && node.children.length > 0) {
const found = node.children.map(child => findDeepest(child as T, level + 1));
found.sort((a, b) => a[0] - b[0]);
return found[0];
} else {
return [level, node];
}
};
return findDeepest(root)[1];
}
}
interface TreeNodeLike {
children?: TreeNodeLike[];
}
I've explained the approach in comments in the source code, but in short: access authorisation data in route.data using router events, and replace <router-outlet> with an error message if access is denied.
I tried to test if I can output something in the middleware only on certain processes. But if I use the following code - process.server seems always to work - also when I enter the route directly via browser. Other outputs are only visible when I change the route via router. I'm using Nuxt in the universal mode. What's happening there?
Actually I want to feed the store from localstorage user data and then redirect the user when this page is a guarded one. This could be only done from process.client where localStorage is defined. Can it be done with middleware at all? And also when entering the page directly?
middleware/test.vue
export default function (context) {
if (process.server) {
console.log('MIDDLEWARE SERVER')
}
if (!process.server) {
console.log('MIDDLEWARE NON-SERVER')
}
if (process.client) {
console.log('MIDDLEWARE CLIENT')
}
if (process.browser){
console.log('MIDDLEWARE BROWSER')
}
}
pages/test.vue
<template>
<h1>Some test Template</h1>
</template>
<script>
export default {
middleware: ['test']
}
</script>
After digging deep into this I found an answer from a Nuxt team member. Obviously this is the intended default behavior of middleware in the universal mode to run on page refresh only on server. The documentation wasn't that clear about it.
The only way to get stored data in the page refresh scenario is to use cookies like this.
//middleware/auth.js
export default function(context) {
context.store.dispatch("initAuth", context.req)
}
Then:
//store/index.js
actions: {
initAuth(vuexContext, req) {
if(req) {
if (!req.headers.cookie) {
return
}
// go get the cookie ;)
}
}
}
I have a simple use case, where my application is using vue-router and vuex. Then store contains a user object which is null in the beginning. After the user is validated from the server it sends back an user object which contains a JWT auth token which is assigned to the user object in the store. Now lets assume that the user came back after 3 hours and tried to visit a route or perform any other action, considering that the auth token has expired by then, what would be the best way to check that(need to call axios post to check it) and redirect user to the login page. My app will have loads of components so I know I can write logic to check the token valid in the mounted hook of each component but that would mean repeating it all of the components. Also I don't want to use the beforeEach navigation guard because I cannot show any visual feedback to the user like checking... or loading....
I do something similar in one of my projects, it's actually deceptively difficult to handle these types of situations, but you can add a beforeEnter guard to your protected routes, then redirect if the authentication failed.
const guard = function(to, from, next) {
// check for valid auth token
axios.get('/api/checkAuthToken').then(response => {
// Token is valid, so continue
next();
}).catch(error => {
// There was an error so redirect
window.location.href = "/login";
})
};
Then on your route you can do:
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
guard(to, from, next);
}
},
You may notice I've used location.href rather than router.push. I do that because my login form is csrf protected, so I need a new csrf_token.
Your other issue is going to be if the user tries to interact with your page without changing the route (i.e. they click a button and get a 401 response). For this I find it easiest to check authentication on each axios request and redirect to login when I receive a 401 response.
In terms of adding a loading spinner during the guard check you can simply add a loading flag to your vuex store then import your store into your router. Honestly though I wouldn't bother, on a decent production server the check will be done so quickly that the user is unlikely to ever see it.
Try Vue.JS Mixins
You can define a Global Mixin and use it via Vue.use(myMixin) - then all Components will inherit this mixin. If you define a mounted or probably better activated hook on the mixin, it will be called on every component.
There you can use everything a component can do - this will point to your component. And if the component also defines a hook itself, the mixin hook of the same type will run before the components own hook.
Or try a single top-level login component
We used a little different solution - we have a single component which handles everything login-related, which exists outside of the router-view in the parent index.html. This component is always active and can hide the div router-view and overlay a loading message or a login-screen. For an intranet-application this component will also use polling to keep the session alive as long as the browser stays open.
You can load of your router-navigation to this component. - So a child-component which wants to trigger a router-navigation just sets a global reactive property navigateTo which is watched by the top level authentication component. This will trigger an authentication check, possibly a login-workflow and after that the top-level component will call $router.push() With this approach you have complete control over any navigation.
You can use interceptors to silently get the auth token when some request happens.
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const rToken = window.localStorage.getItem('rToken');
return axios.post('url/to/get/refresh/token', { rToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('rToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
return axios(originalRequest);
});
}
return Promise.reject(error);
});
Because you use vuex, you can add some state like isLoading or isChecking.
And in your router.beforeEach, you can check and set isLoading or isChecking follow your current checking state. Then you can show loading message follow this state.
In our route.js we check in beforeEnter hooks the user has token or
not.
route.js
{
path: '/dashboard',
name: dashboard,
meta: {
layout: 'home-layout'
},
components: {
default: Dashboard,
header: UserHeader
},
beforeEnter: ifAuthenticated,
}
route.js
const ifAuthenticated = (to, from, next) => {
if (localStorage.getItem(token)) {
next();
return;
}
router.push({
name: 'login',
params: {
returnTo: to.path,
query: to.query,
},
});
};