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