Current routing configuration:
//...
{
path: 'foo/:id',
component: SomeComponent,
canActivate: [SomeGuard]
},
//...
Then in guard I call permission service to get access for component:
#Injectable()
export class SomeGuard implements CanActivate {
constructor(private service: Service) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = parseInt(next.params['id']);
return this.service.getPermissions(id).then(permissions => {
if (permissions.canView) {
return true;
}
return false;
});
}
}
But in the component I utilize the same permissions endpoint, which means I call it twice in a row to get to one page:
//...
constructor(private route: ActivatedRoute,
private service: Service) {
}
ngOnInit() {
this.id = parseInt(this.route.snapshot.params['id']);
this.service.getPermissions(this.id).then(permissions => {
// ...
});
}
//...
So it would be great to just save the permissions data in the route and utilize it by both guard and the component. I tried using resolve, but it turns out resolve only activates after the guards, which is no good. So how can i save permissions data?
This looks like the kind of task for a caching service. Permissions do not change often so they are the perfect candidate for caching. That way even multiple visits to the same resource would not trigger multiple HTTP requests for permission checks.
Edit: Since you need permissions to be loaded each time, you could listen for RouteNavigationStart and clear the cache. If this becomes too cumbersome to maintain in the PermissionsService you could extract the logic into a separate service.
You could something like this in the service you use to get your permissions:
// Permissions service
private permissionCache;
constructor(
router: Router,
) {
// clear cache when a route navigation starts
router.events
.filter(event => event instanceof NavigationStart)
.subscribe(event => this.permissionCache = {})
}
getPermissions(id) {
if (permissionCache[id]) {
return Observable.of(permissionCache[id]);
} else {
// get the permissions
permissionCache[id] = response;
return Observable.of(response);
}
});
Related
I'm working on a small personal app. I'll explain what I did until now and in the end my problem and my question.
I have created a Node server and an Angular app.
When the Angular app is booting I'm checking if the user is logged in (via http get request to the server, the request is made in app.component.ts)
ngOnInit(): void {
this.authService.checkIfUserSignedIn();
}
Inside the checkIfUserSignedIn method after that I'm getting the relevant authentication information I notify to the interested components with the auth state.
this.userAuthDetailsSubject.next(this.userAuthDetails);
Additionally, I'm having an AuthGuard that restrict the entry to the "create-list" component only to authenticated users.
In the AuthGurad I'm checking the auth state:
const authStatus = this.authService.isAuth();
return authStatus;
In the menu html component I have the following code:
<span routerLink="create-list" *ngIf="userIsAuthenticated"> New List</span>
Which works fine.
My problem is when i'm visiting manually localhost:4200/create-list
The AuthGuard is probably loaded before auth state is updated and therefore the user has no access to the "create-list" component, although he is signed in eventually.
I thought about two solutions but I'm not sure if they are good and how to implement them, and would like to hear your opinion.
using localStorage (It may be an overkill solution for this tiny problem)
make the HTTP get request to the server (for the auth state) inside the authGuard or maybe subscribe to an observer in the auth service (if so, how to implement that?)
Any ideas/solutions?
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | import("#angular/router").UrlTree | import("rxjs").Observable<boolean | import("#angular/router").UrlTree> | Promise<boolean | import("#angular/router").UrlTree> {
const authStatus = this.authService.isAuth();
if (authStatus) {
return true;
} else {
this.router.navigate(['/login']);
}
}
auth.service.ts
#Injectable()
export class AuthService {
userAuthDetailsSubject = new Subject<UserAuthDetails>();
userAuthDetails: UserAuthDetails = null;
private isAuthenticated = false;
constructor(#Inject(DOCUMENT) private document: Document, private http: HttpClient) {
};
public isAuth(): boolean {
console.log({
isAuth: this.isAuthenticated
})
return this.isAuthenticated;
}
signIn() {
// redirect to signin..
this.document.location.href = '/auth/google';
}
signOut() {
this.document.location.href = '/auth/logout';
}
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated = true;
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
}
For this particular problem you can make the 'isAuthenticated' field a subject just like 'userAuthDetailsSubject' and update its value when the server responds.
auth.service.ts
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated.next(true); //update the value
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
Now change your authguard so it does not return true or false synchronously.
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
boolean | import("#angular/router").UrlTree |
import("rxjs").Observable<boolean | import("#angular/router").UrlTree>| Promise<boolean | import("#angular/router").UrlTree> {
return this.authService.isAuth().subscribe((logged)=>{
if (logged) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
}
Off topic:
Why do you use import("#angular/router").UrlTree? You can use import like import { UrlTree } from '#angular/router';
CanActive support UrlTree return. return this.router.createUrlTree(['/login']); and not create a new async process in your canActivate
On Topic:
If you call direct link, you have to resolve authentication. If you call link or F5 reload browser will lost every data from memory. If you use any token to auth it be worth saving into localStore and restore from here.
Ofc, After authentication if you open new tab, this new tab will new without auth default like you used F5 on current tab. It lives a separate life for each tabs.
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.
I want to have a bunch of data ready the very first thing after a user logs in my app (keeping it cached in a Service).
I thought implementing this data-loading logic before resolving the parent route the user will be redirected to if login is successful, as a Resolver.
To be more specific: before showing the user's home page I would like to have a list of data already loaded and, if the list is not empty, have the first element of that list set as the selected element by default. So, this means, two Services:
ItemsService: this Service will know how to request the list of items and keep it cached after the first request is done
ItemSelectedService: this Service will know which item has been set as selected at anytime
And the following Resolver:
#Injectable()
export class ItemsResolver implements Resolve<any> {
constructor(
private itemSelectedService: ItemSelectedService,
private itemsService: ItemsService
) { }
resolve() {
this.itemsService.getAll()
.subscribe((items) => {
if (items.length > 0) {
this.itemSelectedService.setAsSelected(items[0]);
}
});
}
}
As #resolve() needs to return an Observable (return this.itemsService.getAll() would have just been enough...) but I'm not returning any because I need to subscribe and call itemSelectedService#setAsSelected() once the item list has been fetched asynchronously... what would be the right way to achieve the desired behavior?
Try giving it a tap
resolve() {
return this.itemsService.getAll()
.pipe(
tap(
filter(items=>items.length > 0)
do(items=>this.itemSelectedService.setAsSelected(items[0]))
)
);
}
do / tap
Transparently perform actions or side-effects, such as logging.
https://www.learnrxjs.io/operators/utility/do.html
You can use flatmap to resolve the observable in chain
#Injectable()
export class ItemsResolver implements Resolve<any> {
constructor(
private itemSelectedService: ItemSelectedService,
private itemsService: ItemsService
) { }
resolve() {
return this.getAll()
.pipe(
flatmap(items => this.setSelectService(item[0]))
)
}
getAll() {
return this.itemsService.getAll();
}
setSelectService(item) {
return this.itemSelectedService.setAsSelected(item);
}
}
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;
}
}));
I have an application where there are two guards (AuthGuard - for logged users, AdminGuard - for admins). The AuthGuard on the first loading makes http request to get the user information from the API. The Problem is when you try to access route with both guards, the AdminGuard does not wait for the AuthGuard to finish with the request and set the user so the AdminGuard can check the role of the user, and the application breaks. I know it breaks because the user is undefined.
I'm looking for a solution on how to make the second guard to wait for the first to finish.
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, AdminGuard]
},
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private http: HttpClient) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.http.get('https://jsonplaceholder.typicode.com/users').map(res => {
console.log('Auth Guard.');
console.log(res);
this.authService.user = {role: 'admin'};
return true;
});
return false;
}
}
#Injectable()
export class AdminGuard implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
console.log('Admin Guard.');
console.log(this.authService.user);
if (this.authService.user.role === 'admin') {
return true;
}
return false;
}
}
Here is a plnker link - http://plnkr.co/edit/EqgruNjogTJvsC1Zt5EN?p=preview
Very important thing to understand is that in AuthGuard you make an asynchronous call and we don't know, when it will be resolved. Other code is synchronous and will be executed immediately without waiting this asynchronous call (it's why the user is undefined).
But you can force AdminGuard to wait, while your HTTP call will be resolved: to do that, you can store Observable Subscription (because you're working with observable, but you can do the same trick with promise too) to AuthService from AuthGuard (where you make your HTTP call) using the following row:
this.authService.subscription$ = this.http.get('https://jsonplaceholder.typicode.com/users');
Now your subscription is in AuthService, all that you need is to subscribe on it in both guards (you're using .map() in your case):
AuthGuard:
return this.authService.subscription$.map(res => {
this.authService.user = {role: 'admin'};
return true;
});
AdminGuard:
return this.authService.subscription$.map(res => {
if (this.authService.user.role === 'admin') {
return true;
}
});
Here is the working plunker:
http://plnkr.co/edit/R2Z26GsSvzEpPdU7tOHO?p=preview
If you see "AuthGuard returns TRUE!" and "AdminGuard returns TRUE!" in your console - everything should work fine. I have also logged this.authService.user variable from both AuthGuard and AdminGuard.