Angular CanLoad guard triggers only once upon first lazy load? - javascript

I'm encountering a strange behaviour (or maybe a wanted one). I have an Angular application where all the modules are lazy loaded.
On one module I have a guard that checks if the decoded user from the JWT is a system admin. If so, the user shall proceed to the section, otherwise it will be redirected in dashboard.
The strange thing is that this thing works only upon the first module load. Then, if I try to logout and access with a user that is not as system admin, the CanLoad guard does not trigger.
I've also tried to implement in the same guard the (CanActivate and CanActivateChild) interfaces, and put the guard on the app-routing.module.ts and on the feature-routing.module.ts modules, respectively on the CanLoad, CanActivate, and CanActivateChild properties of the modules.
The CanActivate CanActivateChild methods never gets called. Never.
While the CanLoad placed on the app-routing.module.ts is called just once.
Here's the is-sys-adm.guard.ts file:
export class SFWIsSysAdmGuard implements CanLoad, CanActivate, CanActivateChild {
public constructor(
private readonly accessSvc: SFWAuthService,
private readonly toastSvc: NbToastrService,
private readonly translateSvc: TranslateService,
private readonly navigationSvc: NavigationService,
) { }
public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
console.log('Can activate child hit');
return this.canLoad(undefined, undefined);
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
console.log('Can activate hit');
return this.canLoad(undefined, undefined);
}
public canLoad(route: Route, segments: UrlSegment[]): boolean {
console.log('Can load hit');
const decodedUser = this.accessSvc.decodedUser;
if (!!decodedUser && !!decodedUser.isSystemAdmin) {
return true;
}
this.navigationSvc.goToDashboard();
this.toastSvc.warning(
this.translateSvc.instant('framework.guards.adm_only.access_denied'),
this.translateSvc.instant('common.access_to_section_denied_ttl'),
);
return false;
}
}
Here's the app-routing.module.ts file:
const routes: Routes = [
{
path: '',
redirectTo: `/${AppRoutes.access}`,
pathMatch: 'full'
},
{
path: AppRoutes.dashboard,
loadChildren: () => import('./dashboard/dashboard.module').then(mod => mod.DashboardModule)
},
{
path: AppRoutes.pins,
loadChildren: () => import('./pins/pins.module').then(mod => mod.PinsModule)
},
{
path: AppRoutes.pinTypes,
loadChildren: () => import('./pin-types/pin-types.module').then(mod => mod.PinTypesModule)
},
{
path: AppRoutes.organizationPickup,
loadChildren: () => import('./organization-picker/organization-picker.module').then(mod => mod.OrganizationPickerModule)
},
{
path: AppRoutes.access,
loadChildren: () => import('./access/access.module').then(mod => mod.AccessModule)
},
{
path: AppRoutes.tourism,
loadChildren: () => import('./tourism/tourism.module').then(mod => mod.TourismModule)
},
{
path: AppRoutes.security,
canLoad: [SFWIsSysAdmGuard],
loadChildren: () => import('./security/security.module').then(mod => mod.SecurityModule)
},
{
path: AppRoutes.notFound,
loadChildren: () => import('./not-found/not-found.module').then(mod => mod.NotFoundModule)
},
{
path: '**',
redirectTo: '/404'
}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Last but not least: feature-routing.module.ts file:
const routes: Routes = [
{
path: '',
canActivate: [SFWIsSysAdmGuard],
component: SecurityComponent,
children: [
{
path: 'tokens-generator',
canActivateChild: [SFWIsSysAdmGuard],
component: TokensGeneratorComponent
},
]
}
];
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SecurityRoutingModule { }
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SecurityRoutingModule { }
Before you ask, I've also tried to put the guard separately in a CanActivate, CanActivateChild, CanLoad to try to prevent any conflict (which shouldn't exist if I've understand the docs.)
Am I missing something? Is is a wanted behaviour or should I open a bug on the official repo?
Thank to anyone willing to spend time on this <3

The CanLoad determines whether the lazy module can be loaded from the server. After it has been loaded, this will not be checked again (unless you press F5). I guess you need to declare it twice, once in CanLoad (if you don't want the code to be loaded at all), and CanActivate, if you want to restrict access.
{
path: AppRoutes.security,
canLoad: [SFWIsSysAdmGuard],
canActivate: [SFWIsSysAdmGuard],
loadChildren: () => import('./security/security.module').then(mod => mod.SecurityModule)
},

Related

Angular - multiple optional route parameters

I can't believe I can't find this situation already covered here in SO:
(I found examples with additional parameter with and without for each single route, but it's unacceptable)
So I have
RouterModule.forRoot([
{
path: 'home',
component: HomeComponent
},
{
path: 'news',
component: NewsComponent
},
{
path: 'newsDetail/:id',
component: NewsDetailComponent
},
...
})
So the example URLs would be
http://somewhere.com/home
http://somewhere.com/news
http://somewhere.com/newsDetail/10
What if I want to add optional parameter to each of those URLs, so I can explicitly call another localization directly in URL (for permalinks):
http://somewhere.com/home/en
http://somewhere.com/news/en
http://somewhere.com/newsDetail/10/en
So it should work with and without "/en" at the end - and of course adding to each and every route (same route with optional /:language) is not the answer (imagine dozens of pages involved, many of them already with their own parameters)
If you want the language parameter to be the first, you can do the following. You will first have to declare an empty app or whatever root component and use this in the bootstrap instead of the AppComponent:
#Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
})
export class RootComponent {}
Then create a module from your current routes, if you do not have that already. Call it AppRoutingModule.
export const AppRoutes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'news', component: NewsComponent },
{ path: 'newsDetail/:id', component: NewsDetailComponent }
];
#NgModule({
imports: [RouterModule.forFeature(AppRoutes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Then create a RootRoutingModule, which will do the magic:
export const RootRoutes: Routes = [
{ path: '', loadChildren: () => import('./app.module').then((m) => m.AppModule) },
{ path: 'en', loadChildren: () => import('./app.module').then((m) => m.AppModule) }
];
#NgModule({
imports: [RouterModule.forRoot(AppRoutes)],
exports: [RouterModule],
})
export class RootRoutingModule {}
The issue with this, is that you'll have to hardcode all the languages you might support, as I don't think a :language parameter will work
So basically, create a root module which will do the language routing and the bootstrapping

Auth0 Angular - Cannot match any route

I have an angular application in which I'm trying to integrate with Auth0. I followed these two tutorials:
https://auth0.com/docs/quickstart/spa/angular2/01-login
https://auth0.com/docs/quickstart/spa/angular2/02-calling-an-api
Here is the setup of my project:
AuthService
Copy+Pasted from the first tutorial link
LoginController
export class LoginComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit(): void {
this.authService.login();
}
}
App-Routing Module
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'profile', component: ProfileComponent, resolve: { queues: ProfileResolver}, canActivate: [AuthGuard]}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: InterceptorService,
multi: true
}
]
})
export class AppRoutingModule { }
I'm getting logged in successfully, however when auth0 does its callback, it navigates to http://localhost:4200/profile?code=[code]&state=[state], which then Angular throws "Cannot match any routes".
My questions are:
What are the code and state parameters used for?
What should I do to properly handle these so my site can route to /profile properly?
Thanks a bunch!
This is a nasty gotcha with auth0-spa-js that caused a big headache for me in production. The issue is describe in this thread. How I solved it was changing the last line of the handleAuthCallback function in the auth service from:
this.router.navigate([targetRoute])
to:
this.router.navigate([name-of-path-you're-redirecting-to]).

Navigate to child of lazy loaded module angular 8

My app-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'updateBooking', loadChildren: () => import('./update-booking/update-booking.module').then(m => m.UpdateBookingModule) },
{ path: 'booking', loadChildren: () => import('./booking/booking.module').then(m => m.BookingModule) }
];
#NgModule({
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }
lazy loaded Booking module's routing:
const routes: Routes = [
{ path: '', redirectTo: 'bookingdashboard' },
{
path: 'bookingdashboard', component: BookingDashboardComponent,
children: [
{ path: '', redirectTo: 'associateinfo' },
{
path: 'associateinfo', component: AssociateInfoComponent
},
{
path: 'agenda', component: AgendaComponent
},
{
path: 'zone', component: ZoneComponent
},
]
}
];
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BookingRoutingModule { }
I am on associate info page with url http://localhost:4200/booking/bookingdashboard/associateinfo
From this page when I try to navigate to agenda page using this._route.navigate(['../agenda' ]); I am getting error Cannot match any routes. URL Segment: 'agenda'
Error: Cannot match any routes. URL Segment: 'agenda'
However If I try to navigate from HTML file using [routerLink]="[ '../agenda']" its navigating to agenda page
You are navigating to only component specific route path, you should follow route as module-path/component-path
this._route.navigate(['booking/bookingdashboard/agenda' ]);
try
this._route.navigate(['/booking/bookingdashboard/agenda']);
After the link parameters array, add an object with a relativeTo
property set to the ActivatedRoute. The router then calculates the
target URL based on the active route's location.
constructor(private _router: Router, private route: ActivatedRoute){}
this._router.navigate(['../agenda'], { relativeTo: this.route })

Infinite console.logging using Ionic 4 Angular Fire Auth guard

I followed the direction in this tutorial to implement a simple auth guard in my Ionic app. It should redirect someone to the 'login' page if they are not logged in.
The problem arises when I go into my 'settings' page and try to log out a current user.
As you can see, there is a 'throttling history state changes to prevent the browser from hanging' that goes on infinitely. In fact, I had to shut down my laptop because the tab wouldn't close. Luckily, I grabbed a troubleshoot screenshot right before I shut it off.
My auth guard code, under app/services/user:
import { Injectable } from '#angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router,
} from '#angular/router';
import { Observable } from 'rxjs';
import * as firebase from 'firebase/app';
import 'firebase/auth';
import { AngularFireModule } from '#angular/fire';
import { environment } from '../../../environments/environment';
firebase.initializeApp(environment.firebase);
#Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | Observable<boolean> | Promise<boolean> {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user: firebase.User) => {
if (user) {
resolve(true);
} else {
this.router.navigateByUrl('/login');
resolve(false);
}
});
});
}
}
The settings.page code, under app/settings:
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { AlertController } from '#ionic/angular';
// Firebase imports
import * as firebase from 'firebase';
import { AngularFireAuth } from '#angular/fire/auth';
import { auth } from 'firebase/app';
#Component({
selector: 'app-settings',
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
constructor(
public alertCtrl: AlertController,
private afAuth: AngularFireAuth,
private router: Router
) { }
ngOnInit() {
}
async logOut() {
const confirm = await this.alertCtrl.create({
header: 'Logging out?',
buttons: [
{
text: 'Yes',
handler: () => {
this.afAuth.auth.signOut();
console.log('Signed out');
}
},
{
text: 'Wait, no',
handler: () => {
console.log('Not clicked');
}
}
]
});
return await confirm.present();
}
}
And, finally, my app-routing.module.ts, under app:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { AuthGuard } from './services/user/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: 'landing', pathMatch: 'full' },
{ path: 'home/:id', loadChildren: './home/home.module#HomePageModule', canActivate: [AuthGuard] },
{ path: 'landing', loadChildren: './landing/landing.module#LandingPageModule', canActivate: [AuthGuard] },
{ path: 'login', loadChildren: './login/login.module#LoginPageModule', canActivate: [AuthGuard] },
{ path: 'signup', loadChildren: './signup/signup.module#SignupPageModule', canActivate: [AuthGuard] },
{ path: 'settings', loadChildren: './settings/settings.module#SettingsPageModule', canActivate: [AuthGuard] },
{ path: 'add-list', loadChildren: './add-list/add-list.module#AddListPageModule', canActivate: [AuthGuard] },
{ path: 'clicked-list', loadChildren: './clicked-list/clicked-list.module#ClickedListPageModule', canActivate: [AuthGuard] },
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
I really can't make sense of it at all. Any help would be much appreciated.
The problem is your AuthGuard is redirecting the route to /login, and on your /login route you have AuthGuard again, this causes an infinite redirection from /login to /login and so on. Remove AuthGuard from /login route it will be fine.

Angular router.navigate w/ route guard not working when already on Component

I am trying to figure out why the router.navigate(['']) code below does not take the user to the login component. I just stay on the home component. When I added debugger; to the code it seems like it goes into some sort of endless loop.
Here is the process:
User comes to site and instantly gets redirected to /login since the Route Guard fails. If they login, Route Guard passes and they go to [' '] which is the HomeComponent. Then when they click logout I figure the navigate to [' '] should fail and simply go to /login. For some reason it stays on the HomeComponent and never navigates.
home.component.ts
logout() {
this.userService.logout();
this.router.navigate(['']);
}
user.service.ts
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.verifyLogin(url);
}
verifyLogin(url: string): boolean {
if (this.userLoggedIn) { return true; }
this.router.navigate(['/login']);
return false;
}
logout() {
this.userLoggedIn = false;
}
app-routing.module.ts
const routes: Routes = [
{ path: '' , component: HomeComponent, canActivate: [UserService]},
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: '**' , redirectTo: '' }
];
For those using Angular 5+, you CAN get router.navigate(['']) to reload the same URL. It's not well documented, and coming from a server heavy framework like .NET, this feels more natural.
Added runGuardsAndResolvers: 'always' to one of your paths and onSameUrlNavigation: 'reload' to the RouterModule initialization.
app-routing.module.ts
const routes: Routes = [
{ path: '' , component: HomeComponent, canActivate: [UserService], runGuardsAndResolvers: 'always' },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: '**' , redirectTo: '' }
];
#NgModule({
imports: [RouterModule.forRoot(routes, {
onSameUrlNavigation: 'reload'
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
Why it should navigate it is already in [' '] and you ask again go to [' ']? You can call 'this.router.navigate(['/login'])' in logout.
logout() {
this.userLoggedIn = false;
this.router.navigate(['/login']);
}

Categories

Resources