]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/auth.js
LP#1746020 Fix Safari/iOS login issue
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / auth.js
1 /* Core Sevice - egAuth
2  *
3  * Manages login and auth session retrieval.
4  */
5
6 angular.module('egCoreMod')
7
8 .factory('egAuth', 
9        ['$q','$timeout','$rootScope','$window','$location','egNet','egHatch',
10 function($q , $timeout , $rootScope , $window , $location , egNet , egHatch) {
11
12     var service = {
13         // the currently active user (au) object
14         user : function(u) {
15             if (u) {
16                 this._user = u;
17             }
18             return this._user;
19         },
20
21         // the user hidden by an operator change
22         OCuser : function(u) {
23             if (u) {
24                 this._OCuser = u;
25             }
26             return this._OCuser;
27         },
28
29         // the Op Change hidden auth token string
30         OCtoken : function() {
31             return egHatch.getLoginSessionItem('eg.auth.token.oc');
32         },
33
34         // Op Change hidden authtime in seconds
35         OCauthtime : function() {
36             return egHatch.getLoginSessionItem('eg.auth.time.oc');
37         },
38
39         // the currently active auth token string
40         token : function() {
41             return egHatch.getLoginSessionItem('eg.auth.token');
42         },
43
44         // authtime in seconds
45         authtime : function() {
46             return egHatch.getLoginSessionItem('eg.auth.time');
47         },
48
49         // the currently active workstation name
50         // For ws_ou or wsid(), see egAuth.user().ws_ou(), etc.
51         workstation : function() {
52             return this.ws;
53         },
54
55         // Listen for logout events in other tabs
56         // Current version of phantomjs (unit tests, etc.) does not 
57         // support BroadcastChannel, so just dummy it up.
58         authChannel : (typeof BroadcastChannel == 'undefined') ? 
59             {} : new BroadcastChannel('eg.auth')
60     };
61
62     /* Returns a promise, which is resolved if valid
63      * authtoken is found, otherwise rejected */
64     service.testAuthToken = function() {
65         var deferred = $q.defer();
66         var token = service.token();
67
68         if (token) {
69
70             if (lf.isOffline && !$location.path().match(/\/session/) ) {
71                 // Just stop here if we're in the offline interface but not on the session tab
72                 $timeout(function(){deferred.resolve()});
73             } else if (lf.isOffline && $location.path().match(/\/session/) && !$window.navigator.onLine) {
74                 // Likewise, if we're in the offline interface on the session tab and the network is down.
75                 // The session tab itself will redirect appropriately due to no network.
76                 $timeout(function(){deferred.resolve()});
77             } else {
78                 // Otherwise, check the token.  This will freeze all other interfaces, which is what we want.
79                 egNet.request(
80                     'open-ils.auth',
81                     'open-ils.auth.session.retrieve', token)
82     
83                 .then(function(user) {
84                     if (user && user.classname) {
85                         // authtoken test succeeded
86                         service.user(user);
87                         service.poll();
88                         service.check_workstation(deferred);
89     
90                     } else {
91                         // authtoken test failed
92                         egHatch.clearLoginSessionItems();
93                         deferred.reject(); 
94                     }
95                 });
96             }
97
98         } else {
99             // no authtoken to test
100             deferred.reject('No authtoken found');
101         }
102
103         return deferred.promise;
104     };
105
106     service.check_workstation = function(deferred) {
107
108         var user = service.user();
109         var ws_path = '/admin/workstation/workstations';
110
111         return egHatch.getItem('eg.workstation.all')
112         .then(function(workstations) { 
113             if (!workstations) workstations = [];
114
115             // If the user is authenticated with a workstation, get the
116             // name from the locally registered version of the workstation.
117
118             if (user.wsid()) {
119
120                 var ws = workstations.filter(
121                     function(w) {return w.id == user.wsid()})[0];
122
123                 if (ws) { // success
124                     service.ws = ws.name;
125                     deferred.resolve();
126                     return;
127                 }
128             }
129
130             if ($location.path() == ws_path) {
131                 // User is on the workstation admin page.  No need
132                 // to redirect.
133                 deferred.resolve();
134                 return;
135             }
136
137             // At this point, the user is trying to access a page
138             // besides the workstation admin page without a valid
139             // registered workstation.  Send them back to the 
140             // workstation admin page.
141
142             // NOTE: egEnv also defines basePath, but we cannot import
143             // egEnv here becuase it creates a circular reference.
144             $window.location.href = '/eg/staff' + ws_path;
145             deferred.resolve();
146         });
147     }
148
149     /**
150      * Returns a promise, which is resolved on successful 
151      * login and rejected on failed login.
152      */
153     service.login = function(args, ops) {
154         // avoid modifying the caller's data structure.
155         args = angular.copy(args);
156
157         if (!ops) { // only set on redo attempts.
158             ops = {deferred : $q.defer()};
159
160             // Clear old LoginSession keys that were left in localStorage
161             // when the previous user closed the browser without logging
162             // out.  Under normal circumstance, LoginSession data would
163             // have been cleared by now, either during logout or cookie
164             // expiration.  But, if for some reason the user manually
165             // removed the auth token cookie w/o closing the browser
166             // (say, for testing), then this serves double duty to ensure
167             // LoginSession data cannot persist across logins.
168             egHatch.clearLoginSessionItems();
169         }
170
171         service.login_api(args).then(function(evt) {
172
173             if (evt.textcode == 'SUCCESS') {
174                 service.handle_login_ok(args, evt);
175                 ops.deferred.resolve({
176                     invalid_workstation : ops.invalid_workstation
177                 });
178
179             } else if (evt.textcode == 'WORKSTATION_NOT_FOUND') {
180                 ops.invalid_workstation = true;
181                 delete args.workstation;
182                 service.login(args, ops); // redo w/o workstation
183
184             } else {
185                 // note: the likely outcome here is a NO_SESION
186                 // server event, which results in broadcasting an 
187                 // egInvalidAuth by egNet. 
188                 console.error('login failed ' + js2JSON(evt));
189                 ops.deferred.reject();
190             }
191         });
192
193         return ops.deferred.promise;
194     }
195
196     /**
197      * Returns a promise, which is resolved on successful 
198      * login and rejected on failed login.
199      */
200     service.opChange = function(args) {
201         // avoid modifying the caller's data structure.
202         args = angular.copy(args);
203         args.workstation = service.workstation();
204
205         var deferred = $q.defer();
206
207         service.login_api(args).then(function(evt) {
208
209             if (evt.textcode == 'SUCCESS') {
210                 if (args.type != 'persist') {
211                     egHatch.setLoginSessionItem('eg.auth.token.oc', service.token());
212                     egHatch.setLoginSessionItem('eg.auth.time.oc', service.authtime());
213                     service.OCuser(service.user());
214                 }
215                 service.handle_login_ok(args, evt);
216                 service.testAuthToken().then(
217                     deferred.resolve,
218                     function () { service.opChangeUndo().then(deferred.reject)  }
219                 );
220             } else {
221                 // note: the likely outcome here is a NO_SESION
222                 // server event, which results in broadcasting an 
223                 // egInvalidAuth by egNet. 
224                 console.error('operator change failed ' + js2JSON(evt));
225                 deferred.reject();
226             }
227         });
228
229         return deferred.promise;
230     }
231
232     service.opChangeUndo = function() {
233         if (service.OCtoken()) {
234             service.user(service.OCuser());
235             egHatch.setLoginSessionItem('eg.auth.token', service.OCtoken());
236             egHatch.setLoginSessionItem('eg.auth.time', service.OCauthtime());
237             egHatch.removeLoginSessionItem('eg.auth.token.oc');
238             egHatch.removeLoginSessionItem('eg.auth.time.oc');
239         }
240         return service.testAuthToken();
241     }
242
243     service.login_api = function(args) {
244         return egNet.request(
245             'open-ils.auth',
246             'open-ils.auth.authenticate.init', args.username)
247         .then(function(seed) {
248                 // avoid clobbering the bare password in case
249                 // we need it for a login redo attempt.
250                 var login_args = angular.copy(args);
251                 login_args.password = hex_md5(seed + hex_md5(args.password));
252
253                 return egNet.request(
254                     'open-ils.auth',
255                     'open-ils.auth.authenticate.complete', login_args)
256             }
257         );
258     }
259
260     service.handle_login_ok = function(args, evt) {
261         service.ws = args.workstation; 
262         egHatch.setLoginSessionItem('eg.auth.token', evt.payload.authtoken);
263         egHatch.setLoginSessionItem('eg.auth.time', evt.payload.authtime);
264         service.poll();
265     }
266
267     /**
268      * Force-check the validity of the authtoken on occasion. 
269      * This allows us to redirect an idle staff client back to the login
270      * page after the session times out.  Otherwise, the UI would stay
271      * open with potentially sensitive data visible.
272      * TODO: What is the practical difference (for a browser) between 
273      * checking auth validity and the ui.general.idle_timeout setting?
274      * Does that setting serve a purpose in a browser environment?
275      */
276     service.poll = function() {
277
278         if (!service.authChannel.onmessage) {
279             // Now that we have an authtoken, listen for logout events 
280             // initiated by other tabs.
281             service.authChannel.onmessage = function(e) {
282                 if (e.data.action == 'logout') {
283                     $rootScope.$broadcast(
284                         'egAuthExpired', {startedElsewhere : true});
285                 }
286             }
287         }
288
289         $timeout(
290             function() {
291                 egNet.request(                                                     
292                     'open-ils.auth',                                               
293                     'open-ils.auth.session.retrieve', 
294                     service.token(),
295                     0, // return extra auth details, unneeded here.
296                     1  // avoid extending the auth timeout
297                 ).then(function(user) {
298                     if (user && user.classname) { // all good
299                         service.poll();
300                     } else {
301                         // NOTE: we should never get here, since egNet
302                         // filters responses for NO_SESSION events.
303                         $rootScope.$broadcast('egAuthExpired');
304                     }
305                 })
306             },
307             // add a 5 second delay to give the token plenty of time
308             // to expire on the server.
309             service.authtime() * 1000 + 5000
310         );
311     }
312
313     service.logout = function(broadcast) {
314
315         if (broadcast && service.authChannel.postMessage) {
316             // Tell the other tabs to shut it all down.
317             service.authChannel.postMessage({action : 'logout'});
318         }
319
320         if (service.token()) {
321             egNet.request(
322                 'open-ils.auth', 
323                 'open-ils.auth.session.delete', 
324                 service.token()); // fire and forget
325             egHatch.clearLoginSessionItems();
326         }
327         service._user = null;
328     };
329
330     return service;
331 }])
332
333
334 /**
335  * Service for testing user permissions.
336  * Note: this cannot live within egAuth, because it creates a circular
337  * dependency of egOrg -> egEnv -> egAuth -> egOrg
338  */
339 .factory('egPerm', 
340        ['$q','egNet','egAuth','egOrg',
341 function($q , egNet , egAuth , egOrg) {
342     var service = {};
343
344     /*
345      * Returns the full list of org unit objects at which the currently
346      * logged in user has the selected permissions.
347      * @permList - list or string.  If a list, the response object is a
348      * hash of perm => orgList maps.  If a string, the response is the
349      * org list for the requested perm.
350      */
351     service.hasPermAt = function(permList, asId) {
352         var deferred = $q.defer();
353         var isArray = true;
354         if (!angular.isArray(permList)) {
355             isArray = false;
356             permList = [permList];
357         }
358         // as called, this method will return the top-most org unit of the
359         // sub-tree at which this user has the selected permission.
360         // From there, flesh the descendant orgs locally.
361         egNet.request(
362             'open-ils.actor',
363             'open-ils.actor.user.has_work_perm_at.batch',
364             egAuth.token(), permList
365         ).then(function(resp) {
366             var answer = {};
367             angular.forEach(permList, function(perm) {
368                 var all = [];
369                 angular.forEach(resp[perm], function(oneOrg) {
370                     all = all.concat(egOrg.descendants(oneOrg, asId));
371                 });
372                 answer[perm] = all;
373             });
374             if (!isArray) answer = answer[permList[0]];
375             deferred.resolve(answer);
376         });
377        return deferred.promise;
378     };
379
380
381     /**
382      * Returns a hash of perm => hasPermBool for each requested permission.
383      * If the authenticated user has no workstation, no checks are made
384      * and all permissions return false.
385      */
386     service.hasPermHere = function(permList) {
387         var response = {};
388
389         var isArray = true;
390         if (!angular.isArray(permList)) {
391             isArray = false;
392             permList = [permList];
393         }
394
395         // no workstation, all are false
396         if (egAuth.user().wsid() === null) {
397             console.warn("egPerm.hasPermHere() called with no workstation");
398             if (isArray) {
399                 response = permList.map(function(perm) {
400                     return response[perm] = false;
401                 });
402             } else {
403                 response = false;
404             }
405             return $q.when(response);
406         }
407
408         ws_ou = Number(egAuth.user().ws_ou()); // from string
409
410         return service.hasPermAt(permList, true)
411         .then(function(orgMap) {
412             angular.forEach(orgMap, function(orgIds, perm) {
413                 // each permission is mapped to a flat list of org unit ids,
414                 // including descendants.  See if our workstation org unit
415                 // is in the list.
416                 response[perm] = orgIds.indexOf(ws_ou) > -1;
417             });
418             if (!isArray) response = response[permList[0]];
419             return response;
420         });
421     }
422
423     return service;
424 }])
425
426