From f3197733a7b2f2dbccd8939432499cd1ef21e782 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 21 Nov 2017 18:41:59 -0500 Subject: [PATCH] LP#626157 Ang2 experiments Signed-off-by: Bill Erickson --- Open-ILS/webby-src/src/app/base.component.ts | 1 - Open-ILS/webby-src/src/app/base.module.ts | 8 +- Open-ILS/webby-src/src/app/core/auth.service.ts | 98 ++++++++++++++++++---- Open-ILS/webby-src/src/app/core/idl.service.ts | 1 - Open-ILS/webby-src/src/app/core/store.service.ts | 48 ++++++----- .../webby-src/src/app/staff/auth-guard.service.ts | 42 ++++++++++ .../src/app/staff/common-data-resolver.service.ts | 28 +++++++ .../webby-src/src/app/staff/login.component.ts | 25 ++++-- .../webby-src/src/app/staff/resolver.service.ts | 34 ++++++++ Open-ILS/webby-src/src/app/staff/routing.module.ts | 43 ++++++++++ .../webby-src/src/app/staff/splash.component.css | 0 .../webby-src/src/app/staff/splash.component.html | 3 + .../webby-src/src/app/staff/splash.component.ts | 17 ++++ .../src/app/staff/staff-resolver.service.ts | 33 -------- .../src/app/staff/staff-routing.module.ts | 29 ------- .../webby-src/src/app/staff/staff.component.ts | 16 +++- Open-ILS/webby-src/src/app/staff/staff.module.ts | 14 ++-- 17 files changed, 320 insertions(+), 120 deletions(-) create mode 100644 Open-ILS/webby-src/src/app/staff/auth-guard.service.ts create mode 100644 Open-ILS/webby-src/src/app/staff/common-data-resolver.service.ts create mode 100644 Open-ILS/webby-src/src/app/staff/resolver.service.ts create mode 100644 Open-ILS/webby-src/src/app/staff/routing.module.ts create mode 100644 Open-ILS/webby-src/src/app/staff/splash.component.css create mode 100644 Open-ILS/webby-src/src/app/staff/splash.component.html create mode 100644 Open-ILS/webby-src/src/app/staff/splash.component.ts delete mode 100644 Open-ILS/webby-src/src/app/staff/staff-resolver.service.ts delete mode 100644 Open-ILS/webby-src/src/app/staff/staff-routing.module.ts diff --git a/Open-ILS/webby-src/src/app/base.component.ts b/Open-ILS/webby-src/src/app/base.component.ts index 953b5e9fae..b72bedb1cc 100644 --- a/Open-ILS/webby-src/src/app/base.component.ts +++ b/Open-ILS/webby-src/src/app/base.component.ts @@ -6,7 +6,6 @@ import { Component } from '@angular/core'; }) export class EgBaseComponent { - title = 'EgBase'; } diff --git a/Open-ILS/webby-src/src/app/base.module.ts b/Open-ILS/webby-src/src/app/base.module.ts index 1fbb2fafb0..27dca353a7 100644 --- a/Open-ILS/webby-src/src/app/base.module.ts +++ b/Open-ILS/webby-src/src/app/base.module.ts @@ -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] }) diff --git a/Open-ILS/webby-src/src/app/core/auth.service.ts b/Open-ILS/webby-src/src/app/core/auth.service.ts index dfb101e2a9..628831f75c 100644 --- a/Open-ILS/webby-src/src/app/core/auth.service.ts +++ b/Open-ILS/webby-src/src/app/core/auth.service.ts @@ -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 { - if (!ops) ops = {}; + // NOTE: EgNetService emits an event if the auth session has expired. + testAuthToken(): Promise { + + 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( (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 { return new Promise((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 { - 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 { 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; + } } diff --git a/Open-ILS/webby-src/src/app/core/idl.service.ts b/Open-ILS/webby-src/src/app/core/idl.service.ts index 3fffd125cb..7d44e1047e 100644 --- a/Open-ILS/webby-src/src/app/core/idl.service.ts +++ b/Open-ILS/webby-src/src/app/core/idl.service.ts @@ -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; diff --git a/Open-ILS/webby-src/src/app/core/store.service.ts b/Open-ILS/webby-src/src/app/core/store.service.ts index caf1625910..653d7e8d75 100644 --- a/Open-ILS/webby-src/src/app/core/store.service.ts +++ b/Open-ILS/webby-src/src/app/core/store.service.ts @@ -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 { @@ -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 { @@ -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 { @@ -60,6 +73,7 @@ export class EgStoreService { } getLoginSessionItem(key: string): any { + return this.parseJson(this.cookieService.get(key)); } removeItem(key: string): Promise { @@ -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 { @@ -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 index 0000000000..f2c276a2f8 --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/auth-guard.service.ts @@ -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 { + + 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 { + 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 index 0000000000..6908cf08db --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/common-data-resolver.service.ts @@ -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> { + + constructor( + private router: Router, + private egAuth: EgAuthService + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + + console.debug('EgStaffCommonDataResolver:resolve()'); + + // TODO verify workstation + + return Promise.resolve(true); + } +} + diff --git a/Open-ILS/webby-src/src/app/staff/login.component.ts b/Open-ILS/webby-src/src/app/staff/login.component.ts index 4c544d7eab..4ae20cfa6a 100644 --- a/Open-ILS/webby-src/src/app/staff/login.component.ts +++ b/Open-ILS/webby-src/src/app/staff/login.component.ts @@ -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 index 0000000000..f697a8acb5 --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/resolver.service.ts @@ -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> { + + constructor( + private router: Router, + private egStore: EgStoreService, + private egAuth: EgAuthService + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + // 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 index 0000000000..82b08f46ae --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/routing.module.ts @@ -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 index 0000000000..e69de29bb2 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 index 0000000000..14cf389d87 --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/splash.component.html @@ -0,0 +1,3 @@ + +Staff Splash Page + 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 index 0000000000..64a54a3a0a --- /dev/null +++ b/Open-ILS/webby-src/src/app/staff/splash.component.ts @@ -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 index e7a70652c8..0000000000 --- a/Open-ILS/webby-src/src/app/staff/staff-resolver.service.ts +++ /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 { - - constructor(private router: Router) {} - - resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable { - - 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 index 55c2c68202..0000000000 --- a/Open-ILS/webby-src/src/app/staff/staff-routing.module.ts +++ /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 {} diff --git a/Open-ILS/webby-src/src/app/staff/staff.component.ts b/Open-ILS/webby-src/src/app/staff/staff.component.ts index 2c13840fe1..2e8460ea38 100644 --- a/Open-ILS/webby-src/src/app/staff/staff.component.ts +++ b/Open-ILS/webby-src/src/app/staff/staff.component.ts @@ -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'); }); } diff --git a/Open-ILS/webby-src/src/app/staff/staff.module.ts b/Open-ILS/webby-src/src/app/staff/staff.module.ts index 1ce80fce0e..529528ded7 100644 --- a/Open-ILS/webby-src/src/app/staff/staff.module.ts +++ b/Open-ILS/webby-src/src/app/staff/staff.module.ts @@ -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: [ -- 2.11.0