1 import {Injectable, EventEmitter} from '@angular/core';
2 import {NetService} from './net.service';
3 import {EventService, EgEvent} from './event.service';
4 import {IdlService, IdlObject} from './idl.service';
5 import {StoreService} from './store.service';
7 // Not universally available.
8 declare var BroadcastChannel;
10 // Models a login instance.
12 user: IdlObject; // actor.usr (au) object
13 workstation: string; // workstation name
17 constructor(token: string, authtime: number, workstation?: string) {
19 this.workstation = workstation;
20 this.authtime = authtime;
24 // Params required for calling the login() method.
25 interface AuthLoginArgs {
32 export enum AuthWsState {
40 @Injectable({providedIn: 'root'})
41 export class AuthService {
43 private authChannel: any;
45 private activeUser: AuthUser = null;
47 workstationState: AuthWsState = AuthWsState.PENDING;
49 // Used by auth-checking resolvers
52 // reference to active auth validity setTimeout handler.
56 private egEvt: EventService,
57 private net: NetService,
58 private store: StoreService
61 // BroadcastChannel is not yet defined in PhantomJS and elsewhere
62 this.authChannel = (typeof BroadcastChannel === 'undefined') ?
63 {} : new BroadcastChannel('eg.auth');
66 // Returns true if we are currently in op-change mode.
67 opChangeIsActive(): boolean {
68 return Boolean(this.store.getLoginSessionItem('eg.auth.time.oc'));
71 // - Accessor functions always refer to the active user.
74 return this.activeUser ? this.activeUser.user : null;
78 workstation(): string {
79 return this.activeUser ? this.activeUser.workstation : null;
83 return this.activeUser ? this.activeUser.token : null;
87 return this.activeUser ? this.activeUser.authtime : 0;
90 // NOTE: NetService emits an event if the auth session has expired.
91 // This only rejects when no authtoken is found.
92 testAuthToken(): Promise<any> {
94 if (!this.activeUser) {
95 // Only necessary on new page loads. During op-change,
96 // for example, we already have an activeUser.
97 this.activeUser = new AuthUser(
98 this.store.getLoginSessionItem('eg.auth.token'),
99 this.store.getLoginSessionItem('eg.auth.time')
104 return Promise.reject('no authtoken');
107 return this.net.request(
109 'open-ils.auth.session.retrieve', this.token()).toPromise()
111 // NetService interceps NO_SESSION events.
112 // We can only get here if the session is valid.
113 this.activeUser.user = user;
114 this.listenForLogout();
119 loginApi(args: AuthLoginArgs, service: string,
120 method: string, isOpChange?: boolean): Promise<void> {
122 return this.net.request(service, method, args)
123 .toPromise().then(res => {
124 return this.handleLoginResponse(
125 args, this.egEvt.parse(res), isOpChange);
129 login(args: AuthLoginArgs, isOpChange?: boolean): Promise<void> {
130 let service = 'open-ils.auth';
131 let method = 'open-ils.auth.login';
133 return this.net.request(
134 'open-ils.auth_proxy',
135 'open-ils.auth_proxy.enabled')
138 if (Number(enabled) === 1) {
139 service = 'open-ils.auth_proxy';
140 method = 'open-ils.auth_proxy.login';
142 return this.loginApi(args, service, method, isOpChange);
145 // auth_proxy check resulted in a low-level error.
146 // Likely the service is not running. Fall back to
147 // standard auth login.
148 return this.loginApi(args, service, method, isOpChange);
154 args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
156 switch (evt.textcode) {
158 return this.handleLoginOk(args, evt, isOpChange);
160 case 'WORKSTATION_NOT_FOUND':
161 console.error(`No such workstation "${args.workstation}"`);
162 this.workstationState = AuthWsState.NOT_FOUND_SERVER;
163 delete args.workstation;
164 return this.login(args, isOpChange);
167 console.error(`Login returned unexpected event: ${evt}`);
168 return Promise.reject('login failed');
172 // Stash the login data
173 handleLoginOk(args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
176 this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
177 this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
180 this.activeUser = new AuthUser(
181 evt.payload.authtoken,
182 evt.payload.authtime,
186 this.store.setLoginSessionItem('eg.auth.token', this.token());
187 this.store.setLoginSessionItem('eg.auth.time', this.authtime());
189 return Promise.resolve();
192 undoOpChange(): Promise<any> {
193 if (this.opChangeIsActive()) {
194 this.deleteSession();
195 this.activeUser = new AuthUser(
196 this.store.getLoginSessionItem('eg.auth.token.oc'),
197 this.store.getLoginSessionItem('eg.auth.time.oc'),
198 this.activeUser.workstation
200 this.store.removeLoginSessionItem('eg.auth.token.oc');
201 this.store.removeLoginSessionItem('eg.auth.time.oc');
202 this.store.setLoginSessionItem('eg.auth.token', this.token());
203 this.store.setLoginSessionItem('eg.auth.time', this.authtime());
205 // Re-fetch the user.
206 return this.testAuthToken();
210 * Listen for logout events initiated by other browser tabs.
212 listenForLogout(): void {
213 if (this.authChannel.onmessage) {
217 this.authChannel.onmessage = (e) => {
219 `received eg.auth broadcast ${JSON.stringify(e.data)}`);
221 if (e.data.action === 'logout') {
222 // Logout will be handled by the originating tab.
223 // We just need to clear tab-local memory.
225 this.net.authExpired$.emit({viaExternal: true});
231 * Force-check the validity of the authtoken on occasion.
232 * This allows us to redirect an idle staff client back to the login
233 * page after the session times out. Otherwise, the UI would stay
234 * open with potentially sensitive data visible.
235 * TODO: What is the practical difference (for a browser) between
236 * checking auth validity and the ui.general.idle_timeout setting?
237 * Does that setting serve a purpose in a browser environment?
239 sessionPoll(): void {
241 // add a 5 second delay to give the token plenty of time
242 // to expire on the server.
243 let pollTime = this.authtime() * 1000 + 5000;
245 if (pollTime < 60000) {
246 // Never poll more often than once per minute.
248 } else if (pollTime > 2147483647) {
249 // Avoid integer overflow resulting in $timeout() effectively
250 // running with timeout=0 in a loop.
251 pollTime = 2147483647;
254 this.pollTimeout = setTimeout(() => {
257 'open-ils.auth.session.retrieve',
259 0, // return extra auth details, unneeded here.
260 1 // avoid extending the auth timeout
262 // NetService intercepts NO_SESSION events.
263 // If the promise resolves, the session is valid.
265 user => this.sessionPoll(),
266 err => console.warn('auth poll error: ' + err)
273 // Resolves if login workstation matches a workstation known to this
274 // browser instance. No attempt is made to see if the workstation
275 // is present on the server. That happens at login time.
276 verifyWorkstation(): Promise<void> {
279 this.workstationState = AuthWsState.PENDING;
280 return Promise.reject('Cannot verify workstation without user');
283 if (!this.user().wsid()) {
284 this.workstationState = AuthWsState.NOT_USED;
285 return Promise.reject('User has no workstation ID to verify');
288 return new Promise((resolve, reject) => {
290 this.store.getLocalItem('eg.workstation.all');
293 const ws = workstations.filter(
294 w => Number(w.id) === Number(this.user().wsid()))[0];
297 this.activeUser.workstation = ws.name;
298 this.workstationState = AuthWsState.VALID;
303 this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
308 deleteSession(): void {
310 // note we have to subscribe to the net.request or it will
311 // not fire -- observables only run when subscribed to.
314 'open-ils.auth.session.delete', this.token())
319 // Tell all listening browser tabs that it's time to logout.
320 // This should only be invoked by one tab.
321 broadcastLogout(): void {
322 console.debug('Notifying tabs of imminent auth token removal');
323 this.authChannel.postMessage({action : 'logout'});
326 // Remove/reset session data
328 this.activeUser = null;
329 if (this.pollTimeout) {
330 clearTimeout(this.pollTimeout);
331 this.pollTimeout = null;
335 // Invalidate server auth token and clean up.
337 this.deleteSession();
338 this.store.clearLoginSessionItems();