Merge "change PLCoreBase to XOSBase"
diff --git a/src/app/core/field/field.html b/src/app/core/field/field.html
index e472939..9baa1d5 100644
--- a/src/app/core/field/field.html
+++ b/src/app/core/field/field.html
@@ -1,4 +1,7 @@
-<label ng-if="vm.field.type !== 'object' && vm.field.type !== 'array'">{{vm.field.label}}</label>
+<label ng-if="vm.field.type !== 'object' && vm.field.type !== 'array'">
+    {{vm.field.label}}
+    <span class="required" ng-if="vm.field.validators.required">*</span>
+</label>
 <input
         xos-custom-validator custom-validator="vm.field.validators.custom || null"
         ng-if="vm.field.type !== 'boolean' && vm.field.type !== 'object' && vm.field.type !== 'select' && vm.field.type !== 'array'"
diff --git a/src/app/core/field/field.scss b/src/app/core/field/field.scss
new file mode 100644
index 0000000..d15f359
--- /dev/null
+++ b/src/app/core/field/field.scss
@@ -0,0 +1,5 @@
+@import "../../style/vars";
+
+span.required {
+  color: $color-accent;
+}
\ No newline at end of file
diff --git a/src/app/core/field/field.ts b/src/app/core/field/field.ts
index ad36473..5c5b48a 100644
--- a/src/app/core/field/field.ts
+++ b/src/app/core/field/field.ts
@@ -1,3 +1,4 @@
+import './field.scss';
 import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
 import {IXosFormHelpersService} from '../form/form-helpers';
 import * as _ from 'lodash';
diff --git a/src/app/core/form/form.html b/src/app/core/form/form.html
index 5da8c4e..4476741 100644
--- a/src/app/core/form/form.html
+++ b/src/app/core/form/form.html
@@ -21,4 +21,7 @@
             {{action.label}}
         </button>
     </div>
+    <div class="form-group">
+        Fields marked with <span class="required">*</span> are <span class="required">required</span> fields.
+    </div>
 </form>
diff --git a/src/app/core/form/form.spec.ts b/src/app/core/form/form.spec.ts
index c5244e0..6b2a196 100644
--- a/src/app/core/form/form.spec.ts
+++ b/src/app/core/form/form.spec.ts
@@ -96,7 +96,9 @@
           name: 'email',
           label: 'Mail:',
           type: 'email',
-          validators: {}
+          validators: {
+            required: true
+          }
         },
         {
           name: 'birthDate',
@@ -170,9 +172,16 @@
 
     // TODO move in xosField test
     it('should set a custom label', () => {
-      let nameField = element[0].getElementsByClassName('form-group')[0];
+      let nameField = element[0].getElementsByClassName('form-group')[1];
       let label = angular.element(nameField.getElementsByTagName('label')[0]).text();
-      expect(label).toEqual('Id:');
+      expect(label).toContain('Name:');
+      expect(label).not.toContain('*');
+    });
+
+    it('should print an * for required fields', () => {
+      let nameField = element[0].getElementsByClassName('form-group')[2];
+      let label = angular.element(nameField.getElementsByTagName('label')[0]).text();
+      expect(label).toContain('*');
     });
 
     // TODO move test in xos-field
diff --git a/src/app/core/loader/loader.ts b/src/app/core/loader/loader.ts
index f12b17b..23acf6a 100644
--- a/src/app/core/loader/loader.ts
+++ b/src/app/core/loader/loader.ts
@@ -37,8 +37,8 @@
       this.$state.go('xos.login');
     }
     else {
-      this.XosModelDiscoverer.discover()
       // NOTE loading XOS Models
+      this.XosModelDiscoverer.discover()
         .then((res) => {
           if (res) {
             this.$log.info('[XosLoader] All models loaded');
@@ -46,9 +46,9 @@
           else {
             this.$log.info('[XosLoader] Failed to load some models, moving on.');
           }
+          // NOTE loading GUI Extensions
           return this.XosOnboarder.onboard();
         })
-        // NOTE loading GUI Extensions
         .then(() => {
           this.moveOnTo(this.XosConfig.lastVisitedUrl);
         })
diff --git a/src/app/core/login/login.ts b/src/app/core/login/login.ts
index 8aa40d5..d47f265 100644
--- a/src/app/core/login/login.ts
+++ b/src/app/core/login/login.ts
@@ -43,7 +43,12 @@
       })
       .catch(e => {
         this.$log.error(`[XosLogin] Error during login.`, e);
-        this.errorMsg = `Something went wrong, please try again.`;
+        if (e.error === 'XOSNotAuthenticated') {
+          this.errorMsg = `This combination of username/password cannot be authenticated`;
+        }
+        else {
+          this.errorMsg = `Something went wrong, please try again.`;
+        }
         this.showErrorMsg = true;
       });
   }
diff --git a/src/app/core/services/keyboard-shortcut.ts b/src/app/core/services/keyboard-shortcut.ts
index 35af07e..1907b0d 100644
--- a/src/app/core/services/keyboard-shortcut.ts
+++ b/src/app/core/services/keyboard-shortcut.ts
@@ -85,6 +85,10 @@
 
       const pressedKey = this.whatKey(e.which);
 
+      if (!pressedKey) {
+        return;
+      }
+
       if (this.allowedModifiers.indexOf(e.key) > -1) {
         this.addActiveModifierKey(e.key);
         return;
diff --git a/src/app/datasources/helpers/model-discoverer.service.ts b/src/app/datasources/helpers/model-discoverer.service.ts
index 4306825..1ef2904 100644
--- a/src/app/datasources/helpers/model-discoverer.service.ts
+++ b/src/app/datasources/helpers/model-discoverer.service.ts
@@ -8,6 +8,7 @@
 import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
 import {IXosRuntimeStatesService, IXosState} from '../../core/services/runtime-states';
 import {IXosModelStoreService} from '../stores/model.store';
+import {IXosAuthService} from '../rest/auth.rest';
 
 export interface IXosModel {
   name: string; // the model name
@@ -37,7 +38,8 @@
     'XosRuntimeStates',
     'XosNavigationService',
     'XosModelStore',
-    'ngProgressFactory'
+    'ngProgressFactory',
+    'AuthService'
   ];
   private xosModels: IXosModel[] = []; // list of augmented model definitions;
   private xosServices: string[] = []; // list of loaded services
@@ -52,7 +54,8 @@
     private XosRuntimeStates: IXosRuntimeStatesService,
     private XosNavigationService: IXosNavigationService,
     private XosModelStore: IXosModelStoreService,
-    private ngProgressFactory: any // check for type defs
+    private ngProgressFactory: any, // check for type defs
+    private AuthService: IXosAuthService
   ) {
     this.progressBar = this.ngProgressFactory.createInstance();
     this.progressBar.setColor('#f6a821');
@@ -236,6 +239,7 @@
           return d.resolve(model);
         },
         err => {
+          this.AuthService.handleUnauthenticatedRequest(err);
           return d.reject(err);
         }
       );
diff --git a/src/app/datasources/helpers/model.discoverer.service.spec.ts b/src/app/datasources/helpers/model.discoverer.service.spec.ts
index 8b17f34..d5a3232 100644
--- a/src/app/datasources/helpers/model.discoverer.service.spec.ts
+++ b/src/app/datasources/helpers/model.discoverer.service.spec.ts
@@ -78,7 +78,8 @@
       .value('XosRuntimeStates', MockXosRuntimeStates)
       .value('XosModelStore', MockXosModelStore)
       .value('ngProgressFactory', MockngProgressFactory)
-      .value('XosNavigationService', MockXosNavigationService);
+      .value('XosNavigationService', MockXosNavigationService)
+      .value('AuthService', {});
 
     angular.mock.module('test');
   });
diff --git a/src/app/datasources/rest/auth.rest.spec.ts b/src/app/datasources/rest/auth.rest.spec.ts
index 35732a3..7a406db 100644
--- a/src/app/datasources/rest/auth.rest.spec.ts
+++ b/src/app/datasources/rest/auth.rest.spec.ts
@@ -87,4 +87,37 @@
       // httpBackend.flush();
     });
   });
+
+  describe('the handleUnauthenticatedRequest method', () => {
+
+    beforeEach(() => {
+      spyOn(service, 'clearUser');
+    });
+
+    it('should logout the user and redirect to login', () => {
+      service.handleUnauthenticatedRequest({
+        error: 'XOSPermissionDenied',
+        fields: {},
+        specific_error: 'test'
+      });
+      expect(service.clearUser).toHaveBeenCalled();
+    });
+
+    it('should catch errors from strings', () => {
+      service.handleUnauthenticatedRequest('{"fields": {}, "specific_error": "failed to authenticate token g09et150o2s25kdzg8t2n9wotvds9jyl", "error": "XOSPermissionDenied"}');
+      expect(service.clearUser).toHaveBeenCalled();
+    });
+
+    it('should not catch other errors', () => {
+      service.handleUnauthenticatedRequest({
+        error: 'XOSProgrammingError',
+        fields: {},
+        specific_error: 'test'
+      });
+      expect(service.clearUser).not.toHaveBeenCalled();
+
+      service.handleUnauthenticatedRequest('some error');
+      expect(service.clearUser).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/src/app/datasources/rest/auth.rest.ts b/src/app/datasources/rest/auth.rest.ts
index 81c4f7d..82f71b6 100644
--- a/src/app/datasources/rest/auth.rest.ts
+++ b/src/app/datasources/rest/auth.rest.ts
@@ -16,12 +16,19 @@
   email: string;
 }
 
+export interface IXosRestError {
+  error: string;
+  specific_error: string;
+  fields: any;
+}
+
 export interface IXosAuthService {
   login(data: IAuthRequestData): Promise<any>;
   logout(): Promise<any>;
   getUser(): any; // NOTE how to define return user || false ???
   isAuthenticated(): boolean;
   clearUser(): void;
+  handleUnauthenticatedRequest(error: IXosRestError | string): void;
 }
 export class AuthService {
 
@@ -29,7 +36,8 @@
     private $http: angular.IHttpService,
     private $q: angular.IQService,
     private $cookies: angular.cookies.ICookiesService,
-    private AppConfig: IXosAppConfig
+    private AppConfig: IXosAppConfig,
+    private $state: angular.ui.IStateService
   ) {
   }
 
@@ -84,4 +92,29 @@
     const session = this.$cookies.get('sessionid');
     return angular.isDefined(session);
   }
+
+  public handleUnauthenticatedRequest(res: IXosRestError | string): void {
+    let err;
+    if (angular.isString(res)) {
+      try {
+        err = JSON.parse(res);
+      } catch (e) {
+        // NOTE if it's not JSON it means that is not the error we're handling here
+        return;
+      }
+    }
+
+    if (angular.isObject(res)) {
+      err = res;
+    }
+
+    if (err && err.error) {
+      switch (err.error) {
+        case 'XOSPermissionDenied':
+          this.clearUser();
+          this.$state.go('login');
+          break;
+      }
+    }
+  }
 }
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 60d2473..950d441 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -33,7 +33,7 @@
       this._collections[modelName] = new BehaviorSubject([]); // NOTE maybe this can be created when we get response from the resource
       this.loadInitialData(modelName, apiUrl);
     }
-    // else manually trigger the next with the last know value to trigger the subscribe method of who's requestiong this data
+    // else manually trigger the next with the last know value to trigger the subscribe method of who's requesting this data
     else {
       this.efficientNext(this._collections[modelName]);
     }
diff --git a/src/interceptors.ts b/src/interceptors.ts
index a1b5df8..82c5f34 100644
--- a/src/interceptors.ts
+++ b/src/interceptors.ts
@@ -8,9 +8,11 @@
 
 export function userStatusInterceptor($state: angular.ui.IStateService, $cookies: ng.cookies.ICookiesService, $q: ng.IQService) {
   const checkLogin = (res) => {
+    // NOTE this interceptor may never be called as the request is not rejected byt the "model-discoverer" service
     switch (res.status) {
       case -1:
       case 401:
+      case 403:
         $cookies.remove('sessionid', {path: '/'});
         $state.go('login');
         return $q.reject(res);
@@ -20,7 +22,6 @@
   };
 
   return {
-    // response: checkLogin,
     responseError: checkLogin
   };
 }