21

I am stuck in a issue that happens when user manually changes the route in browser tab and presses enter. This forces my ui-router/angular2-router to navigate to the state entered by user. I want to prevent this and allow routing only through the flow I have implemented by button clicks in my website.

ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
Peter
  • 10,492
  • 21
  • 82
  • 132
  • not possible without using some dirty way. – Abdul Mannan Mar 29 '16 at 07:04
  • please suggest if any. – Peter Mar 29 '16 at 07:08
  • It is quite possible, please go trhough this link https://github.com/angular-ui/ui-router/wiki.. All you need to listen $stateChnageStart event which angular fire whenever there is a change in navigation and write some logic to stop from navigating. – TheKingPinMirza Mar 29 '16 at 07:10
  • @immirza no, it is not possible because you can simply come to a webpage without being inside of angular yet. – smnbbrv Mar 29 '16 at 07:12
  • It is very much possible, as i said you need to right logic right after listening the event. If something wrong, keep user on same url else navigate to new url. i believe i have done it. – TheKingPinMirza Mar 29 '16 at 07:20
  • Can `$stateChangeStart` get some param sended by the angular router? If true, then you can use the param to judge if the state change fired by angular or manually by the user? – Yan Yang Mar 29 '16 at 07:59
  • @TheKingPinMirza, I want t keep user on same URL(last correct URL) instead of navigating to page not found or something else. Can you please guide in detail? I am using Angular 16. – aru Jul 24 '23 at 10:39

5 Answers5

21

Its 2018! Angular 5 is here and so is the solution to this issue. BAMM its CanActivate Interface that a class can implement to be a guard deciding if a route can be activated.

We can add this functionality and prevent the access to some of our routes based on the conditions we define. A service for eg AuthGuard that implements the CanActivate interface and defines the canActivate method can be added to route configuration.

class Permissions {
  canGoToRoute(user: UserToken, id: string): boolean {
    return true;
  }
}

@Injectable()
class AuthGuard implements CanActivate {
  constructor(private permissions: Permissions, private currentUser: UserToken) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean>|Promise<boolean>|boolean {
    return this.permissions.canGoToRoute(this.currentUser, route.params.id);
  }
}

If we have a route that we want to protect access to against some condition, we add the guard as follows:

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  {
    path: 'heroes',
    canActivate: [AuthGuard],
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

Here the route heroes and all its children have a layer of guard over it. Hence based on the boolean value returned by the AuthGuard service the user will be allowed or denied access to this route.

EugenSunic
  • 13,162
  • 13
  • 64
  • 86
Peter
  • 10,492
  • 21
  • 82
  • 132
  • 2
    How do you detect the navigation is triggered by url changes manually or internal router navigate method? – Samrat Saha Aug 07 '19 at 08:08
  • @SamratSaha you can check the userToken/Permisiions. If its available, then routing must be internal, else the same must be by entering route in url directly. – Peter Aug 07 '19 at 16:13
  • @VishalGulati This guard means that, if you already in the app, it still can directly input URL to navigate, is that right? EX: current in `crisis-center`, enter `/heroes` => successful – vicnoob Apr 14 '20 at 02:46
11

You can import router in the constructor of a guard. This router instance will have the current URL. ActivatedRouteSnapshot and RouterStateSnapshot in canActivate will contain the URL that the user is attempting to access.

The below example prevents users from directly accessing a route from an outside page.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class DirectAccessGuard implements CanActivate {
  constructor(private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    // If the previous URL was blank, then the user is directly accessing this page
    if (this.router.url === '/') {
      this.router.navigate(['']); // Navigate away to some other page
      return false;
    }
    return true;
  }
}

Add this guard to your routing module

{ path: 'signup/:type/:step', component: SignupComponent, canActivate: [DirectAccessGuard] }
Scott Reed
  • 359
  • 2
  • 9
4

Seems to be an old issue but I was also stuck here until I got just a thing working for my application.

What you can do to discourage direct browser url manipulation is :

1) Have a static boolean field in your application exported throughout. Let's say it's Helper.isNextStep (save the file as helper.ts).

export class Helper {
static isNextStep: boolean; }

2) Set this static field to false on a page view (easily done in app.component.ts constructor) as :

  import {Helper} from '../path/to/helper.ts' 

  export class AppComponent {
  constructor() {
  location.onPopState(() => {

    Helper.isNextStep = false;

    })
 }}

3) Have the canActivate guard set up like :

import { Helper } from '../path/to/helper.ts'
import { CanActivate } from '@angular/router/router';
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(public zone: NgZone, public router: Router) {

}
canActivate(): boolean {
if (!Helper.isNextStep) {
  this.zone.run(() => {
    this.router.navigate(['']) //you can redirect user to any page here ( Optional )
  })
  return false;  //block navigation
}
else {
  return Helper.isNextStep || true;  // allow navigation
}
}

4) Have this canActivate guard provided in app.module.ts

providers: [ AuthGuard ]

and app.route.ts :

  {
path: 'step2',
component: ProductOverviewComponent,
canActivate: [AuthGuard]
},

After all this... you simply need to set the Helper.isNextStep equal to true wherever you will use navigation in your app. (For example a button click that calls a function, so before navigating simply set the static field to true )

someButtonClickFunction() {
    Helper.isNextStep = true;
    this.zone.run(() => {
        this.router.navigate(['/step1']);
    });
}

When the next page is loaded it will automatically be set back to false, not allowing url to change.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
danted4
  • 61
  • 3
  • 1
    For the implementation I am doing I made a **smalls changes** to your proposal that avoided some problems with the onPopState. I declared the variable isNextStep in helper.ts as false: `static isNextStep: boolean = false;` and instead of calling `location.onPopState (() => {...});` in the constructor directly I did the `Helper.isNextStep = false;` Which is working very well and now I have full control of the application because I can configure each of the pages to work or not with the guard. – Alejandro Araujo Nov 28 '19 at 15:33
  • @Alejandro Glad that this could help ! Thanks for adding this for the readers. – danted4 Dec 03 '19 at 15:22
0

The $stateChangeStart event is the proper place to handle this. This event will fire when you try to navigate to a URL. At that point you can check if the user is authenticated, and if not, bounce them back to login.

You would hook up the event like this :

angular
  .module('myApp')
  .run(function ($rootScope, $state, authFactory) {
    $rootScope.$on('$stateChangeStart', function () {
      //if user is not logged in
      if(!authFactory.isAuthed()){ 
        $state.go('login')
      }
    })
  });

Hope it helps.

Khalid Hussain
  • 1,675
  • 17
  • 25
M. Junaid Salaat
  • 3,765
  • 1
  • 23
  • 25
  • Thanks for your solution but the thing is the user is authenticated, I just want to prevent him from accessing the states by manually changing the url. This way he may reach a state that should have got data from previous screen in sequence but because of abrupt routing it will be empty. – Peter Mar 29 '16 at 09:29
-2
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        if (this.auth.fnGetToken()) { //check token
            const role = this.auth.fnGetLoginUser(); //get login user data
            if (role.selRole !== 'admin' && (state.url === '/users' || state.url === '/users/')) {
                this.router.navigate(['contacts']);
                return false;
            } else {
                return true;
            }
        } else {
            this.router.navigate(['Login']);
            return false;
        }
    }