app-route icon indicating copy to clipboard operation
app-route copied to clipboard

pre-route hook - ie: to enable authentication required pages

Open ktiedt opened this issue 8 years ago • 7 comments

Add a pre-route handler that is fired everytime a route is tested, it should receive the route parameters that would be set, if the pre-route handler fails.

pre-route handler would fail by passing back a new route object or path (redirect to login page for example) basically, if return is not falsey it should be a failed pre-route check.

ktiedt avatar Apr 18 '16 18:04 ktiedt

@rictic Its a must have feature. I am stuck because of this missing feature. Why there is no response from Polymer team?

ansarizafar avatar Jul 24 '16 13:07 ansarizafar

@ansarizafar I have an app which requires authentication before the user can proceed. I handle that case at the top level by having an app element which contains a session element and a pages element. I use app-route only inside the pages element, and instead use simple lhidden attributes and observer on a shared "user" object to control which is visible. The session element is responsible for checking user authentication (cookie containing a jwt or if not valid then displaying a logon page which it lazyily loads tp perform logon via user entering name and password).

This all works great and separates concerns about logon from any app-route difficulty. This is especially important as I have app-route elements all over the application and I don't want to pre-hook every single one.

akc42 avatar Jul 26 '16 10:07 akc42

@akc42 Could you please share some code.

ansarizafar avatar Jul 26 '16 17:07 ansarizafar

@ansarizafar A bit difficult because its a private project for a customer, but I can give you a flavor by giving you parts of my-app, my-session and my-pages elements. I don't have a public repository that I can point you at. I hope this is enough to demonstrate how to do

The main app, based around the app-header-layout element. my-error element catches windows.onError - formats the stack and sends it to the server for logging. It also displays a paper toast for the timer length to show the user there has been a problem.

<dom-module id="my-app">
  <template>
    <style is="custom-style" include="iron-flex">
 ...
    </style>
    <my-error timer="10000"></my-error>
    <app-header-layout>
      <app-header
        fixed
        effects="waterfall">
        <app-toolbar>
...
          <template is="dom-if" if="[[user.name]]">
...
          </template>
        </app-toolbar>
      </app-header>
      <my-session id="session" user="{{user}}"></my-session>
      <my-pages
        user="[[user]]"
        unresolved
        hidden="[[!isLoggedOn]]" >APPLICATION LOADING</my-pages>
    </app-header-layout>
  </template>
  <script>
    Polymer({
      is: 'my-app',
      properties: {
        user: {
          type: Object,
          value: function() {return {};},
          notify: true,
          observer: '_userChanged'
        },
        isLoggedOn: {
          type: Boolean,
          value: false,
          notify:true
        }
      },
      created: function() {
        window.performance && performance.mark && performance.mark('my-app.created');
        this.removeAttribute('unresolved');
      },
      doLogOff: function() {
        this.$.session.logOff();
      },
      _userChanged: function(user) {
        if (user.name !== undefined && user.name.length > 0) {
          this.isLoggedOn = true;
          window.performance && performance.mark && performance.mark('my-pages.loading');
          this.importHref(this.resolveUrl('my-pages.html'),null,null,true);
        } else {
          this.isLoggedOn = false;
        }
      }
     });
  </script>
</dom-module>

Part of the session. I leave out the logon form, but you can see how its loaded - I am sure you have your own version. I check for a cookies existence, but I return to the server to get a check its contents. I should be a jwt (using the nodejs jwt-simple module) with specific info encoded within it. I validate it on the server though so I don't need to jwt code in the browser. (This is all http/2 so not worried about security too much). The waiting element just puts up a paper spinner when the waiting property is true.

<dom-module id="my-session">
  <template>
    <iron-ajax
      id="validateuser"
      url="/api/validate_user"
      handle-as="json"
      method = "POST"
      body = "{}"
      content-type="application/json"
      on-response="_validated"></iron-ajax>
    <iron-ajax
      id="logoff"
      url="/logoff"
      body="{}"
      handle-as="json"
      method = "POST"
      content-type="application/json"></iron-ajax>
    <my-waiting waiting="[[waiting]]"></my-waiting>
    <template is="dom-if" if="[[needsLogon]]">
      <my-logon user="{{user}}"></my-logon>
    </template>
  </template>
<script>
Polymer({
  is: 'my-session',

  properties: {
    user: {
      type: Object,
      notify: true,
      value: function() {return {};},
      observer: '_userChanged'
    },
    needsLogon: {
      type: Boolean,
      value: false,
      notify: true
    },
    waiting: {
      type: Boolean,
      value: true,
      notify: true
    }
  },
  attached: function() {
    //first see if our cookie exists - because if it does we may already be logged in
    if (document.cookie.match(/^(.*; +)?MYAPP=[^;]+(.*)?$/)) {
      this.$.validateuser.generateRequest();
    } else {
      this.importHref(this.resolveUrl('my-logon.html'),function(){
        this.needsLogon = true;
        this.waiting = false;
      },null,true);
    }
  },
  logOff: function() {
    this.$.logoff.generateRequest(); //Tell server but don't wait to find out if it saw it or not
    this.user = {};
    document.cookie = 'MYAPP=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
    this.importHref(this.resolveUrl('my-logon.html'),function(){
        this.needsLogon = true;
    },null,true);
  },
  _validated: function(e) {
    var response = e.detail.response;
    this.waiting = false;
    if (response) {
      if (response.status) {
        //this user validated, so we are logged on
        this.user = {  // set up our user
           uid: response.uid,
           name: response.name,
... //lots more data about user
        };
       } else {
        this.logOff();
      }
    } else {
      this.logOff();
    }
  },
  _userChanged: function() {
    if(this.user.name) {
      this.needsLogon = false;
      this.waiting = false;
    }
  }
});
</script>
</dom-module>

Finally the pages module. There is some other stuff (I have a lazy load behaviour which calls the homePage and filename functions) not shown, and I only show a flavour of the various pages that I could select. But see this is the first place that I use app-location and app-route.

<dom-module id="my-pages">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>
    <app-location route="{{route}}"></app-location>
    <app-route
        route="{{route}}"
        pattern="/:page"
        data="{{routeData}}"
        tail="{{subRoute}}"
        active="{{active}}"></app-route>
    <iron-pages role="main" selected="[[page]]" attr-for-selected="name">
      <my-menu name="home" user="[[user]]"></my-menu>
      <my-appointments
        name="appointments"
        user="[[user]]"
        route="{{subRoute}}"></my-appointments>
...
      <my-reports
        name="reports"
        route="{{subRoute}}"
        user="[[user]]"></my-reports>
      <user-profile
        name="profile"
        user="{{user}}"></user-profile>
    </iron-pages>
  </template>

  <script>
    Polymer({
      is: 'my-pages',
      properties: {
        user: {
          type: Object,
          notify: true
        },
        subRoute: {
          type: Object,
          notify: true
        }
      },
      behaviors: [
        MYAPP.LazyPage
      ],
      created: function() {
        window.performance && performance.mark && performance.mark('my-pages.created');
        this.removeAttribute('unresolved');
      },
      attached: function() {
        window.performance && performance.mark && performance.mark('my-pages.attached');
      },
      homePage: function() {
        if (this.user.nopass) {
          return 'profile';
        }
        return 'home';
      },
      filename: function(page) {
        switch (page) {
          case 'unconverted':
          case 'enquiries':
          case 'availability':
          case 'contacts':
            return 'my-standin';  //load standing page until real one implemented
          case 'home':
            return false;
          default:
            return 'my-' + page;
        }
      }
    });
  </script>
</dom-module>

akc42 avatar Jul 27 '16 00:07 akc42

@akc42 Thanks for the code. I am also using a similar session element for token based authentication with my own Graphql inspired backend data layer (Restful RPCs) I am curious why you are not using custom event for user logged-In status notification. I am trying to implement role based guards for different routes. I am currently using _routePageChanged but unable to find a way to cancel this event for restricted routes.

ansarizafar avatar Jul 27 '16 03:07 ansarizafar

@ansarizafar I am not using custom events because I don't need to. The approach is just the standard "Mediator Pattern" that was spoken about at the 2015 Polymer Summit. That fact is that the way the session notifies the app about "user" is a custom event ("user-changed") is behind the scenes. It is interesting to note that there is discussion in the Polymer repository about replacing the notify event with a direct setting of the property because it is a massive improvement in performance. By sticking with this sort of approach I will automatically get those improvements if and when they happen.

akc42 avatar Jul 27 '16 06:07 akc42

@akc42 Thanks for the explanation. It makes perfect sense.

ansarizafar avatar Jul 27 '16 06:07 ansarizafar