LP#626157 Ang2 experiments
authorBill Erickson <berickxx@gmail.com>
Tue, 21 Nov 2017 23:41:59 +0000 (18:41 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 11 Dec 2017 17:39:51 +0000 (12:39 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
17 files changed:
Open-ILS/webby-src/src/app/base.component.ts
Open-ILS/webby-src/src/app/base.module.ts
Open-ILS/webby-src/src/app/core/auth.service.ts
Open-ILS/webby-src/src/app/core/idl.service.ts
Open-ILS/webby-src/src/app/core/store.service.ts
Open-ILS/webby-src/src/app/staff/auth-guard.service.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/common-data-resolver.service.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/login.component.ts
Open-ILS/webby-src/src/app/staff/resolver.service.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/routing.module.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/splash.component.css [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/splash.component.html [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/splash.component.ts [new file with mode: 0644]
Open-ILS/webby-src/src/app/staff/staff-resolver.service.ts [deleted file]
Open-ILS/webby-src/src/app/staff/staff-routing.module.ts [deleted file]
Open-ILS/webby-src/src/app/staff/staff.component.ts
Open-ILS/webby-src/src/app/staff/staff.module.ts

index 953b5e9..b72bedb 100644 (file)
@@ -6,7 +6,6 @@ import { Component } from '@angular/core';
 })
 
 export class EgBaseComponent {
-  title = 'EgBase';
 }
 
 
index 1fbb2fa..27dca35 100644 (file)
@@ -7,6 +7,7 @@ import { BrowserModule } from '@angular/platform-browser';
 import { NgModule }      from '@angular/core';
 import { Router }        from '@angular/router'; // Debugging
 import { NgbModule }     from '@ng-bootstrap/ng-bootstrap';
+import { CookieModule }  from 'ngx-cookie'; // import CookieMonster
 
 import { EgBaseComponent }     from './base.component';
 import { EgBaseRoutingModule } from './base-routing.module';
@@ -17,6 +18,7 @@ import { EgEventService } from '@eg/core/event.service';
 import { EgIdlService }   from '@eg/core/idl.service';
 import { EgNetService }   from '@eg/core/net.service';
 import { EgAuthService }  from '@eg/core/auth.service';
+import { EgStoreService } from '@eg/core/store.service';
 
 @NgModule({
   declarations: [
@@ -26,13 +28,15 @@ import { EgAuthService }  from '@eg/core/auth.service';
   imports: [
     EgBaseRoutingModule,
     BrowserModule,
-    NgbModule.forRoot()
+    NgbModule.forRoot(),
+    CookieModule.forRoot()
   ],
   providers: [
     EgEventService,
     EgIdlService,
     EgNetService,
-    EgAuthService
+    EgAuthService,
+    EgStoreService
   ],
   bootstrap: [EgBaseComponent]
 })
index dfb101e..628831f 100644 (file)
@@ -15,7 +15,7 @@ class EgAuthUser {
     token:       String;
     authtime:    Number;
 
-    constructor(token: String, authtime: Number, workstation: String) {
+    constructor(token: String, authtime: Number, workstation?: String) {
         this.token = token;
         this.workstation = workstation;
         this.authtime = authtime;
@@ -40,6 +40,8 @@ export class EgAuthService {
     // again, when the op-change cycle has completed.
     private opChangeUser: EgAuthUser;
 
+    redirectUrl: string;
+
     constructor(
         private egEvt: EgEventService,
         private egNet: EgNetService,
@@ -54,43 +56,75 @@ export class EgAuthService {
 
     // Workstation name.
     workstation(): String { 
-        return this.activeUser.workstation 
+        return this.activeUser.workstation;
     };
 
     token(): String { 
-        return this.activeUser.token 
+        return this.activeUser ? this.activeUser.token : null;
     };
 
     authtime(): Number { 
         return this.activeUser.authtime 
     };
 
-    login(args: EgAuthLoginArgs, ops?: Object): Promise<any> {
-        if (!ops) ops = {};
+    // NOTE: EgNetService emits an event if the auth session has expired.
+    testAuthToken(): Promise<any> {
+
+        this.activeUser = new EgAuthUser(
+            this.egStore.getLoginSessionItem('eg.auth.token'),
+            this.egStore.getLoginSessionItem('eg.auth.time')
+        );
+
+        if (!this.token()) return Promise.reject('no authtoken');
+
+        return new Promise<any>( (resolve, reject) => {
+            this.egNet.request(
+                'open-ils.auth',
+                'open-ils.auth.session.retrieve', this.token())
+            .subscribe(user => {
+                // EgNetService interceps NO_SESSION events.
+                // We can only get here if the session is valid.
+                this.activeUser.user = user;
+                this.sessionPoll();
+                resolve();
+            });
+        });
+    }
+
+    checkWorkstation(): void {
+        // TODO:
+        // Emits event on invalid workstation.
+    }
+
+    login(args: EgAuthLoginArgs, isOpChange?: boolean): Promise<any> {
 
         return new Promise<any>((resolve, reject) => {
             this.egNet.request('open-ils.auth', 'open-ils.auth.login', args)
             .subscribe(res => {
-                let evt = this.egEvt.parse(res);
-                if (evt) {
-                    if (evt.textcode == 'SUCCESS') {
-                        this.handleLoginOk(args, evt, false);
-                        resolve();
-                    }
-                } else {
-                    // Should never get here.
-                    reject();
-                }
+                this.handleLoginResponse(args, this.egEvt.parse(res), isOpChange);
+                resolve(); // TODO: depends on above...
             });
         });
     }
 
-    testAuthToken(): Promise<any> {
-        return Promise.resolve();
+    handleLoginResponse(
+        args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): void {
+
+        switch (evt.textcode) {
+            case 'SUCCESS':
+                this.handleLoginOk(args, evt, isOpChange);
+                break;
+            case 'WORKSTATION_NOT_FOUND':
+                // TODO relogin without workstation and go to ws admin page
+                break;
+            default:
+                // TODO: reject...
+                console.error(`Login returned unexpected event: ${evt}`);
+        }
     }
 
     // Stash the login data
-    handleLoginOk(args: EgAuthLoginArgs, evt: EgEvent, isOpChange: Boolean): void {
+    handleLoginOk(args: EgAuthLoginArgs, evt: EgEvent, isOpChange: boolean): void {
 
         if (isOpChange) {
             this.egStore.setLoginSessionItem('eg.auth.token.oc', this.token());
@@ -110,6 +144,7 @@ export class EgAuthService {
 
     undoOpChange(): Promise<any> {
         if (this.opChangeUser) {
+            this.deleteSession();
             this.activeUser = this.opChangeUser;
             this.opChangeUser = null;
             this.egStore.removeLoginSessionItem('eg.auth.token.oc');                
@@ -119,4 +154,31 @@ export class EgAuthService {
         }
         return this.testAuthToken();
     }
+
+    sessionPoll(): void {
+        // TODO
+    }
+
+
+    deleteSession(): void {
+        if (this.token()) {
+            // fire and forget
+            this.egNet.request(
+                'open-ils.auth',
+                'open-ils.auth.session.delete', this.token());
+        }
+    }
+
+    logout(broadcast?: boolean) {
+
+        if (broadcast) {
+            // TODO
+            //this.authChannel.postMessage({action : 'logout'});
+        }
+
+        this.deleteSession();
+        this.egStore.clearLoginSessionItems();                                  
+        this.activeUser = null;
+        this.opChangeUser = null;
+    }
 }
index 3fffd12..7d44e10 100644 (file)
@@ -31,7 +31,6 @@ export class EgIdlService {
          * Creates the class constructor and getter/setter
          * methods for each IDL class.
          */
-        
         let mkclass = (cls, fields) => {
             this_.classes[cls].classname = cls;
 
index caf1625..653d7e8 100644 (file)
@@ -3,9 +3,29 @@
  */
 import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs/Rx';
+import { CookieService } from 'ngx-cookie';
 
 @Injectable()
 export class EgStoreService {
+    
+    // Base path for cookie-based storage.
+    loginSessionBasePath: string;
+
+    constructor(private cookieService: CookieService) {}
+
+    private parseJson(valJson: string): any {
+        let val: any = null;
+
+        if (valJson != null) {
+            try {
+                val = JSON.parse(valJson);
+            } catch(E) { 
+                console.error(`Failure to parse JSON: ${E} => ${valJson}`);
+            }
+        }
+
+        return val;
+    }
 
     setItem(key: string, val: any, isJson?: Boolean): Promise<any> {
 
@@ -31,6 +51,8 @@ export class EgStoreService {
 
     setLoginSessionItem(key: string, val: any, isJson?:Boolean): void {
         if (!isJson) val = JSON.stringify(val);
+        console.log(`storing ses item ${key} : ${val}`);
+        this.cookieService.put(key, val, {path : this.loginSessionBasePath});
     }
 
     getItem(key: string): Promise<any> {
@@ -40,16 +62,7 @@ export class EgStoreService {
     }
 
     getLocalItem(key: string): any {
-        let valJson: string = window.localStorage.getItem(key);
-        if (valJson === null)  return null;
-        try {
-            return JSON.parse(valJson); 
-        } catch (E) {
-            console.error(`Deleting invalid JSON for localItem: ` 
-                + `${key} => ${valJson} : ${E}`);
-            this.removeLocalItem(key);
-        }
-        return null;
+        return this.parseJson(window.localStorage.getItem(key));
     }
 
     getServerItem(key: string): Promise<any> {
@@ -60,6 +73,7 @@ export class EgStoreService {
     }
 
     getLoginSessionItem(key: string): any {
+        return this.parseJson(this.cookieService.get(key));
     }
 
     removeItem(key: string): Promise<any> {
@@ -67,16 +81,7 @@ export class EgStoreService {
     }
 
     removeLocalItem(key: string): void {
-        let valJson: string = window.localStorage.getItem(key);
-        if (valJson === null)  return null;
-        try {
-            return JSON.parse(valJson); 
-        } catch (E) {
-            console.error(
-                `Deleting invalid JSON for localItem: ${key} => ${valJson}`);
-            this.removeLocalItem(key);
-            return null;
-        }
+        window.localStorage.removeItem(key);
     }
 
     removeServerItem(key: string): Promise<any> {
@@ -84,11 +89,14 @@ export class EgStoreService {
     }
 
     removeSessionItem(key: string): void {
+        this.cookieService.remove(key, {path : this.loginSessionBasePath});
     }
 
     removeLoginSessionItem(key: string): void {
     }
 
+    clearLoginSessionItems(): void {
+    }
 
 }
 
diff --git a/Open-ILS/webby-src/src/app/staff/auth-guard.service.ts b/Open-ILS/webby-src/src/app/staff/auth-guard.service.ts
new file mode 100644 (file)
index 0000000..f2c276a
--- /dev/null
@@ -0,0 +1,42 @@
+import { Injectable }     from '@angular/core';
+import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot,
+    CanActivateChild }    from '@angular/router';
+import { EgAuthService }  from '@eg/core/auth.service';
+
+/**
+ * Confirm we have a valid auth token before allowing access
+ * any staff pages (except for login).
+ */
+
+@Injectable()
+export class EgStaffAuthGuard implements CanActivate, CanActivateChild {
+
+    constructor(private egAuth: EgAuthService, private router: Router) {}
+
+    canActivate(
+        route: ActivatedRouteSnapshot, 
+        state: RouterStateSnapshot): Promise<boolean> {
+
+        console.debug('EgStaffAuthGuard:canActivate()');
+
+        // Note: avoid verifying the workstation here, since the 
+        // workstation admin page requires access to this route.
+        return new Promise((resolve, error) => {
+            this.egAuth.testAuthToken().then(
+                ok  => { resolve(true); },
+                err => {
+                    this.egAuth.redirectUrl = state.url;
+                    this.router.navigate(['/staff/login']);
+                    resolve(false);
+                }
+            );
+        });
+    }
+
+    canActivateChild(
+        route: ActivatedRouteSnapshot, 
+        state: RouterStateSnapshot): Promise<boolean> {
+        return this.canActivate(route, state);
+    }
+}
+
diff --git a/Open-ILS/webby-src/src/app/staff/common-data-resolver.service.ts b/Open-ILS/webby-src/src/app/staff/common-data-resolver.service.ts
new file mode 100644 (file)
index 0000000..6908cf0
--- /dev/null
@@ -0,0 +1,28 @@
+import { Injectable }             from '@angular/core';
+import { Router, Resolve, RouterStateSnapshot,
+         ActivatedRouteSnapshot } from '@angular/router';
+import { EgAuthService }          from '@eg/core/auth.service';
+
+/**
+ * Check auth token and load data required by all staff components.
+ */
+@Injectable()
+export class EgStaffCommonDataResolver implements Resolve<Promise<boolean>> {
+
+    constructor(
+        private router: Router, 
+        private egAuth: EgAuthService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot, 
+        state: RouterStateSnapshot): Promise<boolean> {
+
+        console.debug('EgStaffCommonDataResolver:resolve()');
+
+        // TODO verify workstation
+
+        return Promise.resolve(true);
+    }
+}
+
index 4c544d7..4ae20cf 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, OnInit, Renderer } from '@angular/core';
+import { Router } from '@angular/router';
 import { EgAuthService } from '@eg/core/auth.service';
 
 @Component({
@@ -17,22 +18,32 @@ export class EgStaffLoginComponent implements OnInit {
     workstations = [];
 
     constructor(
+      private router: Router,
       private egAuth: EgAuthService,
       private renderer: Renderer
     ) {}
     
     ngOnInit() {
-      // Focus username
-      this.renderer.selectRootElement('#username').focus();
 
-      // load workstations...
+        // clear out any stale auth data
+        this.egAuth.logout();
+
+        // Focus username
+        this.renderer.selectRootElement('#username').focus();
+
+        // load local workstation data
     }
 
     handleSubmit() {
-      this.egAuth.login(this.args).then(
-        // redirect to desire page.
-        res => console.log('Authtoken: ' + this.egAuth.token())
-      );
+        // where are we go after succesful login
+        let url: string = this.egAuth.redirectUrl || '/staff/splash';
+
+        this.egAuth.login(this.args).then(res => {
+            console.debug('Login succeeded');
+            this.egAuth.redirectUrl = null;
+            console.log(`Routing to URL: ${url}`);
+            this.router.navigate([url]);
+        });
     }
 }
 
diff --git a/Open-ILS/webby-src/src/app/staff/resolver.service.ts b/Open-ILS/webby-src/src/app/staff/resolver.service.ts
new file mode 100644 (file)
index 0000000..f697a8a
--- /dev/null
@@ -0,0 +1,34 @@
+import { Injectable }             from '@angular/core';
+import { Router, Resolve, RouterStateSnapshot,
+         ActivatedRouteSnapshot } from '@angular/router';
+import { EgStoreService }         from '@eg/core/store.service';
+import { EgAuthService }          from '@eg/core/auth.service';
+
+/**
+ * Apply configuration, etc. required by all staff components.
+ * This resolver is called before authentication is confirmed.
+ * See EgStaffCommonDataResolver for staff-wide, post-auth activities.
+ */
+@Injectable()
+export class EgStaffResolver implements Resolve<Promise<boolean>> {
+
+    constructor(
+        private router: Router, 
+        private egStore: EgStoreService,
+        private egAuth: EgAuthService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot, 
+        state: RouterStateSnapshot): Promise<boolean> {
+        // Staff-global configuration, etc.
+
+        console.debug('EgStaffResolver:resolve()');
+
+        // Staff cookies stay in /$base/staff/
+        this.egStore.loginSessionBasePath = '/webby/staff'; // TODO dynamic
+
+        return Promise.resolve(true);
+    }
+}
+
diff --git a/Open-ILS/webby-src/src/app/staff/routing.module.ts b/Open-ILS/webby-src/src/app/staff/routing.module.ts
new file mode 100644 (file)
index 0000000..82b08f4
--- /dev/null
@@ -0,0 +1,43 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { EgStaffResolver } from './resolver.service';
+import { EgStaffCommonDataResolver } from './common-data-resolver.service';
+import { EgStaffAuthGuard } from './auth-guard.service';
+import { EgStaffComponent } from './staff.component';
+import { EgStaffLoginComponent } from './login.component';
+import { EgStaffSplashComponent } from './splash.component';
+
+const routes: Routes = [
+  { path: '',
+    component: EgStaffComponent,
+    resolve : {staffResolver : EgStaffResolver},
+    children : [
+      {   path: 'login',
+          component: EgStaffLoginComponent
+      }, {   
+          path : 'splash',
+          canActivate : [EgStaffAuthGuard],
+          resolve : {commonDataResolver : EgStaffCommonDataResolver},
+          component: EgStaffSplashComponent
+       }, {   
+          path : 'circ',
+          canActivate : [EgStaffAuthGuard],
+          resolve : {commonDataResolver : EgStaffCommonDataResolver},
+          loadChildren : '@eg/staff/circ/circ.module#EgCircModule'
+        }
+    ],
+  }
+];
+
+@NgModule({
+  imports: [ RouterModule.forChild(routes) ],
+  exports: [ RouterModule ],
+  providers: [ 
+    EgStaffResolver,
+    EgStaffCommonDataResolver,
+    EgStaffAuthGuard
+  ]
+})
+
+export class EgStaffRoutingModule {}
+
diff --git a/Open-ILS/webby-src/src/app/staff/splash.component.css b/Open-ILS/webby-src/src/app/staff/splash.component.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Open-ILS/webby-src/src/app/staff/splash.component.html b/Open-ILS/webby-src/src/app/staff/splash.component.html
new file mode 100644 (file)
index 0000000..14cf389
--- /dev/null
@@ -0,0 +1,3 @@
+
+<b>Staff Splash Page</b>
+
diff --git a/Open-ILS/webby-src/src/app/staff/splash.component.ts b/Open-ILS/webby-src/src/app/staff/splash.component.ts
new file mode 100644 (file)
index 0000000..64a54a3
--- /dev/null
@@ -0,0 +1,17 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+
+@Component({
+    styleUrls: ['splash.component.css'],
+    templateUrl: 'splash.component.html'
+})
+
+export class EgStaffSplashComponent implements OnInit {
+    constructor(private route: ActivatedRoute) {}
+
+    ngOnInit() {
+
+    }
+}
+
+
diff --git a/Open-ILS/webby-src/src/app/staff/staff-resolver.service.ts b/Open-ILS/webby-src/src/app/staff/staff-resolver.service.ts
deleted file mode 100644 (file)
index e7a7065..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Injectable }             from '@angular/core';
-import { Observable }             from 'rxjs/Observable';
-import { Router, Resolve, RouterStateSnapshot,
-         ActivatedRouteSnapshot } from '@angular/router';
-
-/** 
- * TODO: import network, etc. and implement startup routines.
- */
-
-@Injectable()
-export class EgStaffResolver implements Resolve<any> {
-
-    constructor(private router: Router) {}
-
-    resolve(
-        route: ActivatedRouteSnapshot, 
-        state: RouterStateSnapshot): Observable<any> {
-
-        console.debug('EgStaffResolver:resolve()');
-
-        // TODO: check auth session
-
-        // async placeholder for staff startup routines
-        return Observable.create(
-            observer => {
-                observer.next(123);
-                observer.complete();
-            }
-        );
-    }
-
-}
-
diff --git a/Open-ILS/webby-src/src/app/staff/staff-routing.module.ts b/Open-ILS/webby-src/src/app/staff/staff-routing.module.ts
deleted file mode 100644 (file)
index 55c2c68..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-import { NgModule }              from '@angular/core';
-import { RouterModule, Routes }  from '@angular/router';
-import { EgStaffComponent }      from './staff.component';
-import { EgStaffLoginComponent } from './login.component';
-import { EgStaffResolver }       from './staff-resolver.service';
-
-const routes: Routes = [
-  { path: '',
-    component: EgStaffComponent,
-    resolve : {startup : EgStaffResolver},
-    children : [
-        { 
-          path: 'login',
-          component: EgStaffLoginComponent
-        }, {   
-          path : 'circ',
-          loadChildren : '@eg/staff/circ/circ.module#EgCircModule'
-        }
-    ]
-  }
-];
-
-@NgModule({
-  imports: [ RouterModule.forChild(routes) ],
-  exports: [ RouterModule ],
-  providers: [ EgStaffResolver ]
-})
-
-export class EgStaffRoutingModule {}
index 2c13840..2e8460e 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
+import { Router, ActivatedRoute } from '@angular/router';
 
 @Component({
   templateUrl: 'staff.component.html'
@@ -7,11 +7,21 @@ import { ActivatedRoute, Router } from '@angular/router';
 
 export class EgStaffComponent implements OnInit {
 
-    constructor(private route: ActivatedRoute) {}
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute) {}
 
     ngOnInit() {
 
-        this.route.data.subscribe((data: { startup : any }) => {
+        console.debug(`EgStaffComponent loading route ${this.router.url}`);
+
+        /*
+        if (this.router.url == '/staff') {
+            this.router.navigate(['/staff/splash']);
+        }
+        */
+
+        this.route.data.subscribe((data: {staffResolver : any}) => {
             console.debug('EgStaff ngOnInit complete');
         });
     }
index 1ce80fc..529528d 100644 (file)
@@ -1,17 +1,19 @@
 import { CommonModule } from '@angular/common';
-import { NgModule }     from '@angular/core';
-import { FormsModule }  from '@angular/forms';
-import { NgbModule }     from '@ng-bootstrap/ng-bootstrap';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 
-import { EgStaffComponent }      from './staff.component';
-import { EgStaffRoutingModule }  from './staff-routing.module';
-import { EgStaffNavComponent }   from './nav.component';
+import { EgStaffComponent } from './staff.component';
+import { EgStaffRoutingModule } from './routing.module';
+import { EgStaffNavComponent } from './nav.component';
 import { EgStaffLoginComponent } from './login.component';
+import { EgStaffSplashComponent } from './splash.component';
 
 @NgModule({
   declarations: [
     EgStaffComponent,
     EgStaffNavComponent,
+    EgStaffSplashComponent,
     EgStaffLoginComponent
   ],
   imports: [