import {Injectable, EventEmitter} from '@angular/core';
-import {Observable} from 'rxjs/Rx';
import {EgNetService} from './net';
import {EgEventService, EgEvent} from './event';
import {EgIdlService, EgIdlObject} from './idl';
import {EgStoreService} from './store';
+// Not universally available.
+declare var BroadcastChannel;
+
// Models a login instance.
class EgAuthUser {
- user: EgIdlObject;
+ user: EgIdlObject; // actor.usr (au) object
workstation: string; // workstation name
token: string;
authtime: number;
@Injectable()
export class EgAuthService {
+ private authChannel: any;
+
private activeUser: EgAuthUser;
// opChangeUser refers to the user that has been superseded during
- // an op-change event. This use will become the activeUser once
- // again, when the op-change cycle has completed.
+ // an op-change event. opChangeUser resumes its status as the
+ // activeUser once the op-change cycle has completed.
private opChangeUser: EgAuthUser;
workstationState: EgAuthWsState = EgAuthWsState.PENDING;
+ // Used by auth-checking resolvers
redirectUrl: string;
+ // reference to active auth validity setTimeout handler.
+ pollTimeout: any;
+
constructor(
private egEvt: EgEventService,
private net: EgNetService,
private store: EgStoreService
- ) {}
+ ) {
+
+ // BroadcastChannel is not yet defined in PhantomJS
+ this.authChannel = BroadcastChannel ?
+ new BroadcastChannel('eg.auth') : {};
+ }
+
- // - Accessor functions alway refer to the active user.
+ // - Accessor functions always refer to the active user.
user(): EgIdlObject {
return this.activeUser ? this.activeUser.user : null;
// Workstation name.
workstation(): string {
- return this.activeUser.workstation;
+ return this.activeUser ? this.activeUser.workstation : null;
};
token(): string {
return this.activeUser ? this.activeUser.token : null;
};
- authtime(): Number {
- return this.activeUser.authtime
+ authtime(): number {
+ return this.activeUser ? this.activeUser.authtime : 0;
};
// NOTE: EgNetService emits an event if the auth session has expired.
if (!this.token()) return Promise.reject('no authtoken');
- return new Promise<any>( (resolve, reject) => {
- this.net.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();
- },
- err => { reject(); }
- );
+ return this.net.request(
+ 'open-ils.auth',
+ 'open-ils.auth.session.retrieve', this.token()).toPromise()
+ .then(user => {
+ // EgNetService interceps NO_SESSION events.
+ // We can only get here if the session is valid.
+ this.activeUser.user = user;
+ this.listenForLogout();
+ this.sessionPoll();
});
}
- checkWorkstation(): void {
- // TODO:
- // Emits event on invalid workstation.
- }
-
login(args: EgAuthLoginArgs, isOpChange?: boolean): Promise<void> {
-
- return new Promise<void>((resolve, reject) => {
- this.net.request('open-ils.auth', 'open-ils.auth.login', args)
- .subscribe(res => {
- this.handleLoginResponse(args, this.egEvt.parse(res), isOpChange)
- .then(
- ok => resolve(ok),
- notOk => reject(notOk)
- );
- });
- });
+ return this.net.request('open-ils.auth', 'open-ils.auth.login', args)
+ .toPromise().then(res => {
+ return this.handleLoginResponse(
+ args, this.egEvt.parse(res), isOpChange)
+ })
}
handleLoginResponse(
return this.testAuthToken();
}
+ /**
+ * Listen for logout events initiated by other browser tabs.
+ */
+ listenForLogout(): void {
+ if (this.authChannel.onmessage) return;
+
+ this.authChannel.onmessage = (e) => {
+ console.debug(
+ `received eg.auth broadcast ${JSON.stringify(e.data)}`);
+
+ if (e.data.action == 'logout') {
+ // Logout will be handled by the originating tab.
+ // We just need to clear tab-local memory.
+ this.cleanup();
+ this.net.authExpired$.emit({viaExternal: true});
+ }
+ }
+ }
+
+ /**
+ * Force-check the validity of the authtoken on occasion.
+ * This allows us to redirect an idle staff client back to the login
+ * page after the session times out. Otherwise, the UI would stay
+ * open with potentially sensitive data visible.
+ * TODO: What is the practical difference (for a browser) between
+ * checking auth validity and the ui.general.idle_timeout setting?
+ * Does that setting serve a purpose in a browser environment?
+ */
sessionPoll(): void {
- // TODO
+
+ // add a 5 second delay to give the token plenty of time
+ // to expire on the server.
+ let pollTime = this.authtime() * 1000 + 5000;
+
+ console.debug('EgAuth session poll at ' + pollTime);
+
+ this.pollTimeout = setTimeout(() => {
+ this.net.request(
+ 'open-ils.auth',
+ 'open-ils.auth.session.retrieve',
+ this.token(),
+ 0, // return extra auth details, unneeded here.
+ 1 // avoid extending the auth timeout
+
+ // EgNetService intercepts NO_SESSION events.
+ // If the promise resolves, the session is valid.
+ ).toPromise().then(user => this.sessionPoll())
+
+ }, pollTime);
}
+
// Resolves if login workstation matches a workstation known to this
// browser instance.
verifyWorkstation(): Promise<void> {
- return new Promise((resolve, reject) => {
- if (!this.user()) {
- this.workstationState = EgAuthWsState.PENDING;
- reject();
- return;
- }
+ if (!this.user()) {
+ this.workstationState = EgAuthWsState.PENDING;
+ return Promise.reject('Cannot verify workstation without user');
+ }
- if (!this.user().wsid()) {
- this.workstationState = EgAuthWsState.NOT_USED;
- reject();
- return;
- }
+ if (!this.user().wsid()) {
+ this.workstationState = EgAuthWsState.NOT_USED;
+ return Promise.reject('User has no workstation ID to verify');
+ }
+ return new Promise((resolve, reject) => {
this.store.getItem('eg.workstation.all')
.then(workstations => {
- if (!workstations) workstations = [];
-
- let ws = workstations.filter(
- w => {return w.id == this.user().wsid()})[0];
-
- if (ws) {
- this.activeUser.workstation = ws.name;
- this.workstationState = EgAuthWsState.VALID;
- resolve();
- } else {
- this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL;
- reject();
+
+ if (workstations) {
+ let ws = workstations.filter(
+ w => {return w.id == this.user().wsid()})[0];
+
+ if (ws) {
+ this.activeUser.workstation = ws.name;
+ this.workstationState = EgAuthWsState.VALID;
+ return resolve();
+ }
}
+
+ this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL;
+ reject();
});
});
}
}
}
- logout(broadcast?: boolean) {
- console.debug('logging out');
+ // Tell all listening browser tabs that it's time to logout.
+ // This should only be invoked by one tab.
+ broadcastLogout(): void {
+ console.debug('Notifying tabs of imminent auth token removal');
+ this.authChannel.postMessage({action : 'logout'});
+ }
- if (broadcast) {
- // TODO
- //this.authChannel.postMessage({action : 'logout'});
+ // Remove/reset session data
+ cleanup(): void {
+ this.activeUser = null;
+ this.opChangeUser = null;
+ if (this.pollTimeout) {
+ clearTimeout(this.pollTimeout);
+ this.pollTimeout = null;
}
+ }
+ // Invalidate server auth token and clean up.
+ logout(): void {
this.deleteSession();
this.store.clearLoginSessionItems();
- this.activeUser = null;
- this.opChangeUser = null;
+ this.cleanup();
}
}
* Each response is relayed via Observable onNext(). The interface is
* the same for streaming and atomic requests.
*/
-import { Injectable, EventEmitter } from '@angular/core';
-import { Observable, Observer } from 'rxjs/Rx';
-import { EgEventService, EgEvent } from './event';
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable, Observer} from 'rxjs/Rx';
+import {EgEventService, EgEvent} from './event';
// Global vars from opensrf.js
// These are availavble at runtime, but are not exported.
superseded : Boolean = false;
// If set, this will be used instead of a one-off OpenSRF.ClientSession.
session? : any;
+ // True if we're using a single-use local session
+ localSession: boolean = true;
// Last EgEvent encountered by this request.
// Most callers will not need to import EgEvent since the parsed
this.params = params;
if (session) {
this.session = session;
+ this.localSession = false;
} else {
this.session = new OpenSRF.ClientSession(service);
}
}
}
+export interface EgAuthExpiredEvent {
+ // request is set when the auth expiration was determined as a
+ // by-product of making an API call.
+ request?: EgNetRequest;
+
+ // True if this environment (e.g. browser tab) was notified of the
+ // expired auth token from an external source (e.g. another browser tab).
+ viaExternal?: boolean;
+}
+
@Injectable()
export class EgNetService {
permFailed$: EventEmitter<EgNetRequest>;
- authExpired$: EventEmitter<EgNetRequest>;
+ authExpired$: EventEmitter<EgAuthExpiredEvent>;
// If true, permission failures are emitted via permFailed$
// and the active request is marked as superseded.
private egEvt: EgEventService
) {
this.permFailed$ = new EventEmitter<EgNetRequest>();
- this.authExpired$ = new EventEmitter<EgNetRequest>();
+ this.authExpired$ = new EventEmitter<EgAuthExpiredEvent>();
}
// Standard request call -- Variadic params version
method : request.method,
params : request.params,
oncomplete : () => {
+
+ // TODO: teach opensrf.js to call cleanup() inside
+ // disconnect() and teach EgPcrud to call cleanup()
+ // as needed to avoid long-lived session data bloat.
+ if (request.localSession)
+ request.session.cleanup();
+
// A superseded request will be complete()'ed by the
// superseder at a later time.
if (!request.superseded)
if (request.service == 'open-ils.pcrud' && statCode == 401) {
// 401 is the PCRUD equivalent of a NO_SESSION event
- this.authExpired$.emit(request);
+ this.authExpired$.emit({request: request});
}
request.observer.error(msg);
}).send();
}
- // Relay response object to the caller for typical/successful responses.
- // Applies special handling to response events that require global attention.
+ // Relay response object to the caller for typical/successful
+ // responses. Applies special handling to response events that
+ // require global attention.
private dispatchResponse(request, response): void {
request.evt = this.egEvt.parse(response);
case 'NO_SESSION':
console.debug(`EgNet emitting event: ${request.evt}`);
request.observer.error(request.evt.toString());
- this.authExpired$.emit(request);
+ this.authExpired$.emit({request: request});
return;
case 'PERM_FAILURE':
import {Injectable} from '@angular/core';
import {Observable, Observer} from 'rxjs/Rx';
-//import {toPromise} from 'rxjs/operators';
import {EgIdlService, EgIdlObject} from './idl';
import {EgNetService, EgNetRequest} from './net';
import {EgAuthService} from './auth';
// => xact_close(commit/rollback)
// => disconnect
wrapXact(mainFunc: () => Observable<EgPcrudResponse>): Observable<EgPcrudResponse> {
- let this_ = this;
-
return Observable.create(observer => {
// 1. connect
this.connect()
// 2. start the transaction
- .then(() => {return this_.xactBegin().toPromise()})
+ .then(() => {return this.xactBegin().toPromise()})
// 3. execute the main body
.then(() => {
res => observer.next(res),
err => observer.error(err),
() => {
- this_.xactClose().toPromise().then(() => {
+ this.xactClose().toPromise().then(() => {
// 5. disconnect
- this_.disconnect();
+ this.disconnect();
// 6. all done
observer.complete();
});
if (!Array.isArray(list)) this.cudList = [list];
- let this_ = this;
-
return this.wrapXact(() => {
return Observable.create(observer => {
- this_.cudObserver = observer;
- this_.nextCudRequest();
+ this.cudObserver = observer;
+ this.nextCudRequest();
});
});
}
* all are done, the cudObserver is resolved.
*/
nextCudRequest(): void {
- let this_ = this;
-
if (this.cudIdx >= this.cudList.length) {
this.cudObserver.complete();
return;
`open-ils.pcrud.${action}.${fmObj.classname}`,
[this.token(), fmObj]
).subscribe(
- res => this_.cudObserver.next(res),
- err => this_.cudObserver.error(err),
- () => this_.nextCudRequest()
+ res => this.cudObserver.next(res),
+ err => this.cudObserver.error(err),
+ () => this.nextCudRequest()
);
};
}