How to use an Observable in Angular route guard? - javascript

In my angular routing module, I am using a base navigation when its the default homepage, now on some condition, I want to decide which route to navigate. This is how my app routing looks.
{ path: '', redirectTo: 'home1', canActivate: [homepageGuard], pathMatch: 'full' },
{ path: 'home1', loadChildren: './lazyloadedmodule1' },
{ path: 'home2', loadChildren: './lazyloadedmodule2' },
{ path: 'home3', loadChildren: './lazyloadedmodule3' },
{ path: 'basehome', loadChildren: './lazyloadedmodule4' }
Now in my route guard, I am calling the subscription like this.
canActivate(): Observable<any> | boolean {
return this._myService.getCurrentApp().subscribe(
(r) => {
if(r === 'app1'){
//navigate to app1homepage
} else if (r === 'app2') {
//navigate to app2homepage
} else {
// navigate to base homepage
}
},
(e) => {
console.log(e);
return false;
}
);
}
This is how my service looks where I call the API.
public getCurrentApp(): Observable<any> {
return this.http.get(this.apiBaseUrl + 'getApp').pipe(
catchError((err: HttpErrorResponse) => {
return throwError(err.error.errorMessage);
}));
}
Firstly the routeguard is not taking the value as subscription, because I believe its expecting just a boolean, but i will be getting the response as string back. How do I make sure that during the first page load, its calling the API, and redirecting it accordingly?

I propose something like this. Use service and return true/false & navigate.
canActivate(): Observable<any> | boolean {
return this._myService.getCurrentApp()
pipe(
tap((response) => {
if(r === 'app1'){
this.router.navigate([...]);
} else if (r === 'app2') {
this.router.navigate([...]);
} else {
this.router.navigate([...]);
}
}),
map(() => false),
catchError((e)=> of(false))
)
}
}
Frankly writing, that's not the best solution for such issue. I believe that a better approach would be to create different AuthGuards for the different path which will do API request and check if it is allowed to go to a specific route. Then only return false or true.

Your guard should not subscribe to the observable, the router does that....
So you basically need to return an observable that returns a value of true or false.
If you're going to reroute, then you will obviously never return true.... try this....
canActivate(): Observable<boolean> {
return this._myService.getCurrentApp().pipe(
tap(r => {
if(r === 'app1'){
//navigate to app1homepage
} else if (r === 'app2') {
//navigate to app2homepage
} else {
// navigate to base homepage
}
}),
catchError(r => of(false))
);
}

Related

load data in ngrx effect with dispatch an action before using route guard

My auth is based on NGRX
so when the page starts loading I got all roles and
then get logged in.
but when I start using route guard,
route gourd start working before user data get loading
how can I wait for user load action to be done then start using canActivate
I try below solution but it's not working
export class AuthGuard implements CanActivate, OnDestroy {
private unsubscribe: Subject<any>;
constructor(private store: Store<AppState>, private router: Router,
private alertService: ToastrService, private authService: AuthService) {
this.unsubscribe = new Subject();
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
getFromStoreOrAPI(): Observable<any> {
return this.store.pipe(
select(isUserLoaded),
tap((data: boolean) => {
if (!data) {
this.store.dispatch(new UserRequested());
}
}),
take(1)
);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.getFromStoreOrAPI().pipe(
switchMap(() =>
this.store.pipe(
select(isLoggedIn),
map(loggedIn => {
if (!loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
} else {
this.store.pipe(
select(currentUserRoles),
map((userRoles: Role[]) => {
//.......
}),
takeUntil(this.unsubscribe)
).subscribe();
}
}),
)
),
catchError(() => this.router.navigateByUrl('/auth/login'))
);
}
}
You can use filter to wait until loaded flag is true.
Here is approach I took with my auth.guard.ts :
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authFacade.loaded$.pipe(
filter(loaded => !!loaded),
mergeMap(() => this.authFacade.userAccount$),
map(userAccount => {
if (!userAccount) this.authFacade.redirectLoginPage(state.url);
return !!userAccount;
}),
first()
);
}
In my case, main app component is dispatching an action CheckAuth to check if user is already authenticated, and then set loaded flag.
It should work with some adaptation for your need. But main difference is the use of filter which avoid to continue the workflow if user checking is not done, and force waiting for the value.
Of course, be sure to set loaded value in all the case after receiving response (authenticated or not), or in case of any error.
Here is a potential adaptation for your case :
authLoaded$ = this.store.pipe(select(authLoaded));
authAccount$ = this.store.pipe(select(authAccount));
canActivate(...) {
return userLoaded$.pipe(
tap(loaded => {
if (!loaded) {
this.store.dispatch(new UserRequested());
}
}),
filter(loaded => !!loaded),
mergeMap(() => authAccount$),
map(authAccount => {
if (!authAccount.loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!authAccount.roles?.length) {
this.router.navigateByUrl('/auth/forbidden');
return false;
}
// do some extra stuff...
return true;
}),
first()
);
}
I renamed isUserLoaded to authLoaded to clearly indicate the status of authentication loading (you can use also ready or not for instance). But not necessary user.
I created also a new selector authAccount which returns an object with at least 2 things :
loggedIn : true/false if user is logged in
roles: array of user roles.
But you can add of course user property, with user details.
This is a composed selector from different parts of your state.
With it, your code is more clear and maintable, you receive a complete status of your current authentication user.
Maybe some typos is possible, I wrote the code directly in my answer without testing it.
Hope this will help you.

How to add query param before navigation start in angular

Let's say I have two components A and B. Component A navigates to /a?random=random and B navigates to /b. I want to add authUser query param before navigation starts so end result should look like this /a?random=random&authUser=1, /b?authUser=1 respectively.
The problem is that url for component A looks like this /a?authUser=1 after appending the authUser queryParam not /a?random=random&authUser=1.
Here is the stackblitz link.
// App component
ngOnInit() {
this.router.events.subscribe((event) => {
if(event instanceof NavigationStart) {
if (!event.url.includes('authUser')) {
// remove queryParams
const url = event.url.split('?')[0];
if (url !== '/') {
// /a/etc => ['a','etc']
const newUrl = url.split('/').filter(_subUrl => _subUrl);
this.router.navigate(newUrl, { queryParams: { authUser: '1' }, queryParamsHandling: 'merge' });
}
}
}
})
}
navigateToA() {
this.router.navigate(['a'], { queryParams: { random: 'random' } });
}
navigateToB() {
this.router.navigate(['b']);
}
I don't wanna make changes to component A and B navigate methods because in real applications then i'll have to make changes to every navigate method which would be a bad practice.
exampe fo query paramthis.router.navigate([/${this.appStatus.baseUrl}/predict/result], {queryParams: {vsgid: vsgid, sid: sid, logo: logo}});

how to conditionally subscribe to another observable on success of first observable?

I m tring to use concatmap and complete the observables sequentially when queryParams has id it should only call getbearer tokens or else it should navigate to error with securekey but in reality its going to error page with bearer-token
const result = this.activatedRoute.queryParams.pipe(
concatMap((params) => {
if (params.hasOwnProperty('id')) {
sessionStorage.setItem('secureKey', params.id);
return this.httpServiceService.getBearerTokens();
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'secure-key' },
});
}
})
);
result.subscribe(
(response: any) => {
if (response.access_token) {
sessionStorage.setItem('bearerToken', response.access_token);
this.router.navigate(['summary']);
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
},
(error) => {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
);
I think concatMap is just asking for a race condition (I don't know how often and how easy the query parameters change, though). I would use switchMap instead, because I imagine that the moment we navigate from "?id=alice" to "?id=bob", the matter of Alice's token becomes immaterial.
Generally, conditionally subscribing to another stream should work just fine with switchMap and filter:
firstStream$.pipe(
filter(result => someCondition(result)),
switchMap(result => getAnotherStream(result))
).subscribe(//...
Oh, and by the way, your concatMap returns a stream only if (params.hasOwnProperty('id')). Does that even compile? Return an observable in the else branch as well, it could even be EMPTY, but probably should be throwError.

Angular 5: guard forward retrieved data to next component/route

In an angular 5 app, there is a route guard that check from an API if an object exists:
//guard.ts excerpt
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.clientService.get(next.params.id).switchMap( data => {
return Observable.of(true);
})
.catch( err => Observable.of(false))
}
//route.ts excerpt
{ path: ':id', canActivate: [ ClientDetailGuard ], component: ClientDetail }
this works perfect, but I am wondering if is there a way to pass the data retrieved from my service to next the route/component (ClientDetail), so I won't need to call the service again this again.
I tried to add
next.data.client = data;
before the return of Observable(true) but in the component, the ActivatedRoute's data does not have this value set.
Or should I use something like Resolve?
I know I can achieve this using some state container or a shared service to store/retrieve data, but I wouldn't like to do this at this time, as long as the app is not complex.
I could do this using a Resolver instead of a guard
//route.ts
{ path: ':id', resolve: { client: ClientDetailResolver }, component: ClientDetail }
//resolver.ts
#Injectable()
export class ClientDetailResolver implements Resolve {
constructor(private clientService: ClientService, private router: Router, public location: Location) {
}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any {
return this.clientService.get(route.params.id)
.catch( err => {
//handle error
const path = this.location.path();
this.router.navigate(["error", err.status], { skipLocationChange: true })
.then( () => {
this.location.replaceState(path);
});
return Observable.empty();
})
}
}
You seem to be under-estimating the power of services. Services are the best way to save/store data or states between components. You can set the data from any component, pull the data from any component. You don't have to worry about putting data in for the next route, instead you go to the next route and subscribe to your data on ngOnInit and boom, got everything you need. Here is an example of just how simple it really is.
Example of service
import { Injectable } from '#angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
#Injectable()
export class AppService {
alertData = new BehaviorSubject({
type: '',
message: '',
timer: 0,
showing: false
});
constructor() {}
setAlertData(data: AlertModel) {
this.alertData.next(data);
}
}
Example of using service
this.subscription$.push(this._as.alertData.subscribe((data: AlertModel) => {
this.alertData = data;
if (data.showing) {
this.alertIsShowing = true;
}
else {
this.alertIsShowing = false;
}
}));

AngularJS 2: new router and templateProvider practice from Angular 1

I'm new to angular 2. In angular 1 i used ui-router and templateProvider to use different templates in one controller, for example:
templateProvider: function ($localStorage) {
if($localStorage.isAdmin) { //just an example, here could be any condition, like url has some params
return '<admin></admin>';
} else if($localStorage.isManager) {
return '<manager></manager>';
}
},
how can i achieve something similar in angular 2?
for example i have such routes:
{ path: 'customers/:id', component: CustomerDetailsComponent },
{ path: 'customers/:id/edit', component: CustomerDetailsComponent }
how can i in CustomerDetailsComponent check it?
like:
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
if (params['edit']) { //how to add a condition? to split edit from show view
/*someTemplateLogic*/
}
});
}
is it possible to do?
so: two different templates for routes:
{ path: 'customers/:id', component: CustomerDetailsComponent },
{ path: 'customers/:id/edit', component: CustomerDetailsComponent }
with edit and without - how to check which route i have, and set such template in one component
<admin *ngIf="isAdmin"></admin
<manager *ngIf="!isAdmin"></manager>
ngOnInit(): void {
this.router.events
.filter(f => f instanceof NavigationEnd)
.forEach(e => this.isAdmin = e.url.endsWith('/edit'));
}

Categories

Resources