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