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 if (isOpChange && this.opChangeIsActive()) {
134 // Enforce one op-change at a time.
138 return this.net.request(
139 'open-ils.auth_proxy',
140 'open-ils.auth_proxy.enabled')
143 if (Number(enabled) === 1) {
144 service = 'open-ils.auth_proxy';
145 method = 'open-ils.auth_proxy.login';
147 return this.loginApi(args, service, method, isOpChange);
150 // auth_proxy check resulted in a low-level error.
151 // Likely the service is not running. Fall back to
152 // standard auth login.
153 return this.loginApi(args, service, method, isOpChange);
159 args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
161 switch (evt.textcode) {
163 return this.handleLoginOk(args, evt, isOpChange);
165 case 'WORKSTATION_NOT_FOUND':
166 console.error(`No such workstation "${args.workstation}"`);
167 this.workstationState = AuthWsState.NOT_FOUND_SERVER;
168 delete args.workstation;
169 return this.login(args, isOpChange);
172 console.error(`Login returned unexpected event: ${evt}`);
173 return Promise.reject('login failed');
177 // Stash the login data
178 handleLoginOk(args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
181 this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
182 this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
185 this.activeUser = new AuthUser(
186 evt.payload.authtoken,
187 evt.payload.authtime,
191 this.store.setLoginSessionItem('eg.auth.token', this.token());
192 this.store.setLoginSessionItem('eg.auth.time', this.authtime());
194 return Promise.resolve();
197 undoOpChange(): Promise<any> {
198 if (this.opChangeIsActive()) {
199 this.deleteSession();
200 this.activeUser = new AuthUser(
201 this.store.getLoginSessionItem('eg.auth.token.oc'),
202 this.store.getLoginSessionItem('eg.auth.time.oc'),
203 this.activeUser.workstation
205 this.store.removeLoginSessionItem('eg.auth.token.oc');
206 this.store.removeLoginSessionItem('eg.auth.time.oc');
207 this.store.setLoginSessionItem('eg.auth.token', this.token());
208 this.store.setLoginSessionItem('eg.auth.time', this.authtime());
210 // Re-fetch the user.
211 return this.testAuthToken();
215 * Listen for logout events initiated by other browser tabs.
217 listenForLogout(): void {
218 if (this.authChannel.onmessage) {
222 this.authChannel.onmessage = (e) => {
224 `received eg.auth broadcast ${JSON.stringify(e.data)}`);
226 if (e.data.action === 'logout') {
227 // Logout will be handled by the originating tab.
228 // We just need to clear tab-local memory.
230 this.net.authExpired$.emit({viaExternal: true});
236 * Force-check the validity of the authtoken on occasion.
237 * This allows us to redirect an idle staff client back to the login
238 * page after the session times out. Otherwise, the UI would stay
239 * open with potentially sensitive data visible.
240 * TODO: What is the practical difference (for a browser) between
241 * checking auth validity and the ui.general.idle_timeout setting?
242 * Does that setting serve a purpose in a browser environment?
244 sessionPoll(): void {
246 // add a 5 second delay to give the token plenty of time
247 // to expire on the server.
248 let pollTime = this.authtime() * 1000 + 5000;
250 if (pollTime < 60000) {
251 // Never poll more often than once per minute.
253 } else if (pollTime > 2147483647) {
254 // Avoid integer overflow resulting in $timeout() effectively
255 // running with timeout=0 in a loop.
256 pollTime = 2147483647;
259 this.pollTimeout = setTimeout(() => {
262 'open-ils.auth.session.retrieve',
264 0, // return extra auth details, unneeded here.
265 1 // avoid extending the auth timeout
267 // NetService intercepts NO_SESSION events.
268 // If the promise resolves, the session is valid.
270 user => this.sessionPoll(),
271 err => console.warn('auth poll error: ' + err)
278 // Resolves if login workstation matches a workstation known to this
279 // browser instance. No attempt is made to see if the workstation
280 // is present on the server. That happens at login time.
281 verifyWorkstation(): Promise<void> {
284 this.workstationState = AuthWsState.PENDING;
285 return Promise.reject('Cannot verify workstation without user');
288 if (!this.user().wsid()) {
289 this.workstationState = AuthWsState.NOT_USED;
290 return Promise.reject('User has no workstation ID to verify');
293 return new Promise((resolve, reject) => {
294 return this.store.getWorkstations().then(workstations => {
297 const ws = workstations.filter(
298 w => Number(w.id) === Number(this.user().wsid()))[0];
301 this.activeUser.workstation = ws.name;
302 this.workstationState = AuthWsState.VALID;
307 this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
313 deleteSession(): void {
315 // note we have to subscribe to the net.request or it will
316 // not fire -- observables only run when subscribed to.
319 'open-ils.auth.session.delete', this.token())
324 // Tell all listening browser tabs that it's time to logout.
325 // This should only be invoked by one tab.
326 broadcastLogout(): void {
327 console.debug('Notifying tabs of imminent auth token removal');
328 this.authChannel.postMessage({action : 'logout'});
331 // Remove/reset session data
333 this.activeUser = null;
334 if (this.pollTimeout) {
335 clearTimeout(this.pollTimeout);
336 this.pollTimeout = null;
340 // Invalidate server auth token and clean up.
342 this.deleteSession();
343 this.store.clearLoginSessionItems();