I am trying to fix an issue users are having after impersonating as a different user in the system. Currently, a regional user could impersonate as someone else which does not load the full content of the page as they now have to press F5 to see everything on this page. I tried to reproduce this issue by pointing to the same database in local but not able to do so. When I go to the DEV url for instance, I then try impersonating as a different user which then loads the page partially and refreshing the page (F5), I see the entire content. I believe this is happening due to the route navigate, not sure if I am missing anything to pass in this function.
this.router.navigate(['/', user.Company.Abbreviation.toLowerCase()]);
This is the full function that the Impersonate button is executing.
setImpersonatation() {
this.store.dispatch(new AuditSearchClearAction());
this.store.dispatch(new CompanyChangedAction(this.impersonationForm.value.company, null));
const user = this.impersonationForm.value.user as UserLogin;
this.store.dispatch(new BaseUserSetAction(user as User));
this.store.dispatch(new AuthorizeAction(user));
this.router.navigate(['/', user.Company.Abbreviation.toLowerCase()]);
this.store.dispatch(new MyWorkSetLetterReminderUserAction(user.UserId));
}
When I switch a user, I get the screen below.
But when I refresh the page (F5), then i see the entire data.
Do I need to add any parameters to the router.navigate so it loads the page correctly? Seems like something is missing of when trying to load the page after the Impersonate button is clicked.
Thanks!
UPDATE:
setImpersonatation() {
this.store.dispatch(new AuditSearchClearAction());
this.store.dispatch(new CompanyChangedAction(this.impersonationForm.value.company, null));
const user = this.impersonationForm.value.user as UserLogin;
this.store.dispatch(new BaseUserSetAction(user as User));
this.store.dispatch(new AuthorizeAction(user));
this.store.dispatch(new MyWorkSetLetterReminderUserAction(user.UserId));
**this.changeDetector.detectChanges();
this.router.navigate(['/', user.Company.Abbreviation.toLowerCase()]);**
}
Using a resolver is always a good choice in cases that data is not ready to generate the page UI.
Resolver is preventing your app from navigate unless the data is fetched completely from server.
Steps:
create a resolver service by running this command:
ng generate resolver test-resolver
you have to put your ajax call in the resolve method of resolver:
#Injectable({ providedIn: 'root' })
export class TestResolver implements Resolve<Hero> {
constructor(private service: TestService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any {
return this.service.testAjax();
}
}
you have to define your resolver in your routing module:
#NgModule({
imports: [
RouterModule.forRoot([
{
path: 'test',
component: TestComponent,
resolve: {
test: TestResolver
}
}
])
],
exports: [RouterModule]
})
export class AppRoutingModule {}
you have to catch the response in your component (in this case TestComponent):
#Component({
selector: 'app-test',
template: ``
})
export class TestComponent {
constructor(private activatedRoute: ActivatedRoute) {
this.activatedRoute.data.subscribe((data) => {
console.log(data.test);
});
}
}
In this way you will navigate to the next page ONLY if data is fully loaded and this feature makes it so much easier to implement a better user experience like a loading on navigation to those pages that require some data to be fetched.
Related
In my Angular app, I have list and details pages and I want to lkeep the pageIndex value before navigating to details page. There is a Back button in the details page and I can return the list page by clicking on that button. However, I want to get the pageIndex value when navigating back to the list page and let the user open the page where he/she was before. For example I navigate 3rd page on the list and click details. At this stage I set the pageIndex to 3 and then navigate to details. Then by clicking the Back button I can navigate back to the list page, but I need to retrieve the pageIndex as 3.
So, what is an elegant way to fix this problem in Angular 10+?
list-componnet
details(id) {
this.router.navigate(['/details'], { state: { id: id } }); // I pass id value of the record
}
details-componnet
constructor(private router: Router) {
this.id = this.router.getCurrentNavigation().extras.state.id;
}
back() {
this._location.back();
}
Just write a simple example to make it work, I use the sessionStorage and router together, use router to show your the routing module, actually you can just use sessionStorage only, and wrapper it in a servive. Then you can retrieve the pageIndex anywhere.
And if you want to use router only, the pageIndex paramater will be place in both list and detail component, since this two component all need to use this value, in list component you will need pageIndex to set data-table, in detail component you need this value to pass to list component when redirect back triggered.
The routing module like below:
import { NgModule } from "#angular/core";
import { Routes, RouterModule } from "#angular/router";
import { ListComponent } from "./list/list.component";
import { DetailComponent } from "./detail/detail.component";
const routes: Routes = [
{ path: "", redirectTo: "list", pathMatch: "full" },
{
path: "list/:pageIndex=1",
component: ListComponent,
pathMatch: "full"
},
{
path: "detail/:id",
component: DetailComponent
}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
Here you can navigate to list page from detail page use:
this.router.navigate(["/list/" + pageIndex]);
And then in list page's ngOnInit method to set current pageIndex to your data-table. Here is the demo: https://stackblitz.com/edit/angular-ivy-5vmteg?file=src/app/list/list.component.ts
Use sessionStorage, a listService or router queryParams to keep track of the current pageIndex.
I´d advocate for queryParams as it seems most logical and you can also link directly to specific page.
constructor(
private route: ActivatedRoute,
) { }
// Access the queryParam in list component
// Ie /list?pageIndex=4
this.route.queryParams.subscribe(params => {
// Do something with params.pageIndex if it exists
}
);
I´d also consider to change the way you handle routing to the details. If the route to the list is /list then route to the details should be /list/<listid> so you can link directly to the details if needed.
You can access the listId parameter as below but note it must also be specified as parameter in the router definition.
// Router definition
{ path: 'list/', component: ListComponent},
{ path: 'list/:listId', component: ListIdComponent}
// Access the listId in the details component
this.route.params.subscribe(param=> {
// Do somthing with param.listId
});
I want to reload a route but without reloading the entire page. This is not what I want:
window.location.reload();
I want to reload just the component not the page. This is waht I tried:
Angular: Change the route without reloading
Angular: Refetch data on same URL navigation
Some other random tricks also I tried. Tried following code from this URL:
Angular Basics: Refresh an Angular Component without reloading the same Component
mySubscription: any;
constructor() {
this.router.routeReuseStrategy.shouldReuseRoute=function() {
return false;
}
this.mySubscription= this.router.events.subscribe((event)=> {
if(event instanceof NavigationEnd) {
// Trick the Router into believing it's last link wasn't previously loaded
this.router.navigated=false;
}
})
}
But It's not working due to other reasons. I need a solution. Please suggest something else.
the skipLocationChange option works for me,try my service
import { Injectable } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class ReloadRouteService {
constructor(
private router: Router,
) { }
redirectTo(url: string): void {
// When skipLocationChange true, navigates without pushing a new state into history.
this.router.navigateByUrl('/', {skipLocationChange: true}).then(() => {
this.router.navigate([url]);
});
}
}
scenario :
Here i am building an application in which currently i have a page called products-info where a url is sent to the customer and on clicking on the link he can directly open that particular product page and see the info and the same page will be there in application.
issue :
Here i am protecting the routes using Auth guards so without logging, a user cannot access the page .If i sent the same page url to user via email he should be able to view only the page.
So my application has:
login page
products page
products-info page
Normally if a user logs in, this page will be visible but when an admin sends a url as mobiledot.com/products-info to user's email, user clicks on that and application don't want to login and it doesn't want to show any logout or other pages info only on that specific page. below is my code :
router.module.ts
const routes: Routes = [
{ path: '', component: LoginComponent },
{ path: 'main/:role', component: MainComponent, canActivate: [RouteGuard] },
{ path: 'admin', component: AdminComponent},
{ path: 'user', component: userComponent,canActivate: [RouteGuard]}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: [RouteGuard]
})
export class AppRoutingModule { }
auth guard
#Injectable()
export class RouteGuard implements CanActivate {
constructor(private service: accessService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable<boolean> | Promise<boolean> {
const isAllowed = this.service.getAuthenticated();
if (!isAllowed) {
this.router.navigate(['/']);
}
return isAllowed;
}
}
So i also thought about some thing like if user logs into application. My router module is:
if(user loginin){
{ path: 'main/:role', component: MainComponent, canActivate: [RouteGuard] },
}
else {
{ path: 'main/:token', component: MainComponent },
ex: www.mobiledot.com/product-info?toke="ssdsdsdsdSDSD"
}
Is it possible or do we have any other way?
In short, if admin sent the same page url in application which is protected by auth guards to the user via email then user will click on the link and open the page and it should not ask for login.
There is another problem which is about stored token in localstorage. so before moving, do we have to clear that token and place a new one so that it will not redirect to main page?
You mean that products-info should be visible for registered users, as they can receive the corresponding link inside their email while they may be unauthorized (perhaps by token expiration) so you need a solution to cover it.
First of all, this is a bad practice to reveal sth like token as Url parameter. So avoid it.
Second, Please try to employ right solution instead of changing basic behaviors in your App. AuthGuard designed to decide if a route can be activated or not. It uses browser storage and in case of token expiration, You can only refresh it before expiration, otherwise user has to login again.
My solution is creating sth like a ticket id which can be appended to the sent link (like mobiledot.com/products-info/928f5f8b663571b7f3d829921c9079939c2d0319). You can make it one-time valid or more for users. store it in database and validate it when a user uses. In this way, there's no need to employ AuthGuard just control content permission on server-side.
As you have a route guard , you need a case where it's should return true even you are not logged in.
So, simply you can use the approach you have think of www.mobiledot.com/product-info?toke="ssdsdsdsdSDSD".
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable<boolean> | Promise<boolean> {
// check if there is a query param , like token in the url and url is your without token one.
if(queryParamToken && urlpath === 'view'){
// as you have token hit the api to check if required to check token is a valid one.
or can simply return true as per your need / requirement
-- checkout this link if you need referece
} else {
const isAllowed = this.service.getAuthenticated();
if (!isAllowed) {
this.router.navigate(['/']);
}
return isAllowed;
}
}
Verify token with API in Angular8 canActivate method of AuthGuard -- checkout this link if you need reference
I am building an application using Angular 7, I have handled the API calls, the JWT Token authentication system using C#, and also updating the LocalStorage() when necessary, when the user logs in and logs out, and all these are working perfectly.
My problem is I want it to run a login check as a middleware within the application rather than on the lifecycle method - ng.onInit(). How do I go about this?
Is there a way to execute lifecycle events as an entry component or service. That is, before any component loads it is able to check if the user is logged in or not and redirect via Router to a desired page.
Guard is based on the routes... so I think you should prefer a module/service solution.
import { APP_INITIALIZER } from '#angular/core';
then add it as a provider like this :
export function initApp(initService: YourInitService) {
return () => {
initService.Init();
}
}
{ provide: APP_INITIALIZER,useFactory: initApp, deps: [YourInitService], multi: true }
Routing Decisions Based on Token Expiration
If you’re using JSON Web Tokens (JWT) to secure your Angular app (and I recommend that you do), one way to make a decision about whether or not a route should be accessed is to check the token’s expiration time. It’s likely that you’re using the JWT to let your users access protected resources on your backend. If this is the case, the token won’t be useful if it is expired, so this is a good indication that the user should be considered “not authenticated”.
Create a method in your authentication service which checks whether or not the user is authenticated. Again, for the purposes of stateless authentication with JWT, that is simply a matter of whether the token is expired. The JwtHelperService class from angular2-jwt can be used for this.
// src/app/auth/auth.service.ts
import { Injectable } from '#angular/core';
import { JwtHelperService } from '#auth0/angular-jwt';
#Injectable()
export class AuthService {
constructor(public jwtHelper: JwtHelperService) {}
// ...
public isAuthenticated(): boolean {
const token = localStorage.getItem('token');
// Check whether the token is expired and return
// true or false
return !this.jwtHelper.isTokenExpired(token);
}
}
Note: This example assumes that you are storing the user’s JWT in local storage.
Create a new service which implements the route guard. You can call it whatever you like, but something like auth-guard.service is generally sufficient.
// src/app/auth/auth-guard.service.ts
import { Injectable } from '#angular/core';
import { Router, CanActivate } from '#angular/router';
import { AuthService } from './auth.service';
#Injectable()
export class AuthGuardService implements CanActivate {
constructor(public auth: AuthService, public router: Router) {}
canActivate(): boolean {
if (!this.auth.isAuthenticated()) {
this.router.navigate(['login']);
return false;
}
return true;
}
}
The service injects AuthService and Router and has a single method called canActivate. This method is necessary to properly implement the CanActivate interface.
The canActivate method returns a boolean indicating whether or not navigation to a route should be allowed. If the user isn’t authenticated, they are re-routed to some other place, in this case a route called /login.
Now the guard can be applied to any routes you wish to protect.
// src/app/app.routes.ts
import { Routes, CanActivate } from '#angular/router';
import { ProfileComponent } from './profile/profile.component';
import {
AuthGuardService as AuthGuard
} from './auth/auth-guard.service';
export const ROUTES: Routes = [
{ path: '', component: HomeComponent },
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard]
},
{ path: '**', redirectTo: '' }
];
The /profile route has an extra config value now: canActivate. The AuthGuard that was created above is passed to an array for canActivate which means it will be run any time someone tries to access the /profile route. If the user is authenticated, they get to the route. If not, they are redirected to the /login route.
Note: The canActivate guard still allows the component for a given route to be activated (but not navigated to). If we wanted to prevent activation altogether, we could use the canLoad guard.
more info here
You should check for Guard in angular, especially canActivate Guard: https://angular.io/guide/router
A guard is created like this:
#Injectable({
providedIn: 'root'
})
export class MyGuard implements CanLoad {
constructor() {}
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> |
Promise<boolean> | boolean {
const x = true;
if (x) {
return true; // It allows access to the route;
} else {
// redirect where you want;
return false; // it doesnt allow to access to the route
}
}
}
Then in your routing Module:
{
path: "yourRoute",
canActivate: [MyGuard],
component: YourComponent
}
For authentication, you have a good library that uses guard here:
https://www.npmjs.com/package/ngx-auth
You should implement an authGuardService or something like that to use as middleware for your routing (using the canActivate section)
See: https://angular.io/api/router/CanActivate
This prevents routes from being loaded if the canActivate fails the condition (which is preferred when using a login system etc instead of checking in lifecycle hooks).
Using angular 2 with lazy loaded modules, I can receive(for example) 401 HTTP code from server
bootstrap 0b40fee…:101 GET http://localhost:8082/2.chunk.js
Error: Loading chunk 2 failed.
at HTMLScriptElement.onScriptComplete (bootstrap 0b40fee…:91)
at HTMLScriptElement.wrapFn (zone.js:1032)
at ZoneDelegate.invokeTask (zone.js:414)
at Object.onInvokeTask (core.es5.js:4119)
at ZoneDelegate.invokeTask (zone.js:413)
at Zone.runTask (zone.js:181)
at HTMLScriptElement.ZoneTask.invoke (zone.js:476)
How to handle this error?
Check my answer for details
Workaround to bypass this chunk fails error => Programmatically force app to reload if chunks failed error occurs using global error handler.
import { ErrorHandler } from '#angular/core';
#Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: any): void {
const chunkFailedMessage = /Loading chunk [\d]+ failed/;
if (chunkFailedMessage.test(err.message)) {
window.location.reload();
}
}
}
Provide it in our root module to change default behavior in our app, so instead of using default ErrorHandler class we are using our custom GlobalErrorHandler class.
#NgModule({
providers: [{provide: ErrorHandler, useClass: GlobalErrorHandler}]
})
I was having the same problem so I investigated. I found the solution. This happened to me when I redeployed to another server and the chunk had a [hash].
You can catch the error either in a catch all like this:
ngOnInit() {
if (!this.previousRouterErrorHandler) {
this.previousRouterErrorHandler = this.router.errorHandler;
let that = this;
this.router.errorHandler = function (err: any) {
// Handle here. err.message == "Loading chunk chunk-name failed."
return that.previousRouterErrorHandler.apply(that.previousRouterErrorHandler, arguments);
};
}
}
Or directly at the link which navigated
click() {
this.router.navigate(['/lazy-route'])
.catch(err => {
// Handle here
});
}
Here is my solution for this. I inject this service as a singleton in my app / core module.
It waits for instances of NavigationError from the router, checks if they are ChunkLoadError's and then does a redirect to the place the user wanted to go to anyway.
// Angular
import { Injectable, OnDestroy } from '#angular/core';
import { Router, NavigationError } from '#angular/router';
// Rxjs
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
#Injectable()
export class ChunkErrorHandler implements OnDestroy {
private subscription: Subscription;
constructor(router: Router) {
this.subscription = router.events
.pipe(filter(event => event instanceof NavigationError))
.subscribe(event => {
this.handleRouterErrors(event as NavigationError);
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
private handleRouterErrors(event: NavigationError) {
if (event.error.name === 'ChunkLoadError') {
window.location.href = `${window.location.origin}${event.url}`;
}
}
}
It happen when when deploy new code.The manifest.js which holds the files and hashes doesn't update without refreshing and when it loads a chunk it obviously uses the old hash from manifest.js.
So while catching error we can do force reload with given url :-
click() {
this.router.navigate(['/lazy-route'])
.catch(err => {
// Handle here
// reload with given route
// window.location.pathname('/lazy-route');
// OR
// reset existing route(containing query params) with given route and force reload
window.history.pushState({}, document.title, '/lazy-route' );
window.location.reload();
});
}
chunk related errors can be raised by any environment or routing related issues making them hard to debunk.
In my case, the amount of data moving in my PWA was too much to handle by the angular router. It was flooding the headers of the js chunks getters and therefore raising bad_request errors.
I suggest you to check out those network calls (getters of chunks.js like http://localhost:xxxx/158.js) for anything unusual in headers and refactor sketchy stuff in your current dev environment, since it's a real black hole time to investigate the source of the error by yourself.
Hope that'll help
check out Catch Storage, i guess service worker save some thing in catch storage
console.log(Object.entries(error));
this help me to understand what's inside the error is
rejection,
promise,
zone,
task
and below is my solution:
handleError(error) {
switch (error?.rejection?.name) {
case 'ChunkLoadError':
window.location.href = window.location.href;
break;
default:
break;
}
}
In my case, I was putting my files in an S3 bucket. I kept getting this error because it was calling the wrong filenames all together and returning an html error response.
At some point I let the IT team know what was happening. They were like, let's invalidate the cache on CloudFront... What?! Yeah! Let's do that...
Moral of the story, if you've been searching the web for answers to this error and can't find any, check with the IT team or any place that the index.html file might be getting cached.
this probably means unhandled exception. you have to handle error responses (4xx, 5xx status codes) from server in whatever way you want: show error message somewhere, redirect to some page, do anything but not leave it unhandled.
for example:
return this.http.get(requestDetails)
.map(res => res.json())
.catch(err => {
console.log('server error:', err)
Observable.throw(err);
});