Check for session token before accessing Library Account API
[sitka/overdrive-evergreen-opac.git] / src / od_api.coffee
CommitLineData
8ebc3fff 1define [
b63fc3d5 2 'jquery-noconflict'
8ebc3fff
SC
3 'lodash'
4 'json'
5 'cookies'
6 'moment'
7 'od_config'
bf67304d 8 'od_session'
9669700c
SC
9 'od_data'
10], ($, _, json, C, M, config, Session, D) ->
8ebc3fff
SC
11
12 # Dump the given arguments or log them to console
13 log = ->
14 try
15 dump "#{x}\n" for x in arguments
16 return
17 catch
18 console.log arguments
19 return
20
21 $notify = $ {}
22
23 logError = (jqXHR, textStatus, errorThrown) ->
24 log "#{textStatus} #{jqXHR.status} #{errorThrown}"
25 $notify.trigger 'od.fail', arguments
26
27 # Define custom event names for this module. A custom event is triggered
28 # whenever result data becomes available after making an API request.
29 eventList = [
30 'od.clientaccess'
9669700c 31 'od.libraryinfo'
8ebc3fff
SC
32 'od.metadata'
33 'od.availability'
34
35 'od.patronaccess'
36 'od.patroninfo'
37 'od.holds'
38 'od.checkouts'
39 'od.interests'
40 'od.action'
41
42 'od.hold.update'
43 'od.hold.delete'
44 'od.checkout.update'
45 'od.checkout.delete'
46
47 'od.prefs'
48 'od.login'
49 'od.logout'
50 'od.error'
51 ]
63192819
SC
52 eventObject = $({}).on eventList.join(' '), (e, x, y...) ->
53 # Uncomment for debugging on console
54 #log e.namespace, x, y
8ebc3fff 55
bf67304d
SC
56 # On page load, we unserialize the text string found in local storage into
57 # an object, or if there is no string yet, we create the default object.
8ebc3fff
SC
58 # The session object uses a local storage mechanism based on window.name;
59 # see
60 # http://stackoverflow.com/questions/2035075/using-window-name-as-a-local-data-cache-in-web-browsers
61 # for pros and cons and alternatives.
bf67304d 62 session = new Session window.name, Boolean C('eg_loggedin') or window.IAMXUL
8ebc3fff 63
bf67304d
SC
64 # On window unload, we serialize it into local storage so that it survives
65 # page reloads.
66 $(window).on 'unload', -> window.name = session.store()
8ebc3fff
SC
67
68 # Customize the plain jQuery ajax to post a request for an access token
69 _api = (url, data) ->
70
71 $.ajax $.extend {},
72 # The Basic Authorization string is always added to the HTTP header.
73 headers: Authorization: "Basic #{config.credentials}"
508513a1 74 url: url
8ebc3fff
SC
75 type: 'POST'
76 # We expect data to be always given; the ajax method will convert
77 # it to a query string.
78 data: data
79
8ebc3fff
SC
80 # Convert a serialized array into a serialized object
81 serializeObject = (a) ->
82 o = {}
83 $.each a, ->
84 v = @value or ''
85 if (n = o[@name]) isnt undefined
86 o[@name] = [n] unless n.push
87 o[@name].push v
88 else
89 o[@name] = v
90 return o
91
92 # TODO unused
93 $.fn.extend
94
95 # Convert this serialized array to a serialized object
96 _serializeObject: -> serializeObject @serializeArray()
97
98 # Serialize this to a json string, an object, an array, a query string, or just return itself
99 _serializeX: (X) ->
100 switch X
101 when 'j' then json.stringify @_serializeX 'o'
102 when 'k' then json.stringify @_serializeX 'a'
103 when 'p' then $.param @_serializeX 'a'
104 when 's' then @serialize()
105 when 'o' then serializeObject @_serializeX 'a'
106 when 'a' then @serializeArray()
107 else @
108
8ebc3fff
SC
109
110 # We define the public interface of the module
111 # TODO wrap od in jquery so that we can use it to trigger events and bind event handlers
112 od =
113
114 # Povides the anchor object for implementing a publish/subscribe
115 # mechanism for this module.
116 $: eventObject.on
117
118 # Notification that there are possible changes of values from
119 # preferences page that should be updated in the session cache
bf67304d 120 'od.prefs': (ev, x) -> session.prefs.update x
8ebc3fff
SC
121
122 # Expire patron access token if user is no longer logged into EG
123 'od.logout': (ev, x) ->
124 if x is 'eg'
bf67304d 125 session = new Session() if session.token.is_patron_access()
8ebc3fff
SC
126
127 log: log
128
8ebc3fff
SC
129 # Map format id to format name using current session object
130 labels: (id) -> session.labels[id] or id
131
132 # Customize the plain jQuery ajax method to handle a GET or POST method
133 # for the Overdrive api.
8546cdf5 134 api: (url, method, data, beforeSend) ->
8ebc3fff
SC
135
136 # Do some pre-processing of data before it is sent to server
137 if method is 'post'
138
139 # Convert numberOfDays value from an ISO 8601 date string to
140 # number of days relative to now. There are two subtleties
141 # regarding rounding errors: First, we use only use now of
142 # resolution to days to avoid a local round-down from 1 to 0.
143 # Second, we need to add one to avoid a round-down at the OD
144 # server.
145 for v in data.fields when v.name is 'numberOfDays'
146 v.value = 1 + M(v.value).diff M().toArray()[0..2], 'days'
147
148 $.ajax $.extend {},
149 # The current Authorization string is always added to the HTTP header.
bf67304d 150 headers: Authorization: "#{session.token.token_type} #{session.token.access_token}"
508513a1 151 url: url
8ebc3fff
SC
152 # Will default to 'get' if no method string is supplied
153 type: method
154 # A given data object is expected to be in JSON format
155 contentType: 'application/json; charset=utf-8'
156 data: json.stringify data
8546cdf5 157 beforeSend: beforeSend
8ebc3fff
SC
158
159 .done ->
160
161 # For a post method, we get a data object in reply. We publish
162 # the object using an event named after the data type, eg,
163 # 'hold', 'checkout'. We can't easily recognize the data type
164 # by looking at the data, so we have to pattern match on the
165 # API URL.
166 if method is 'post'
167 if /\/holds|\/suspension/.test url
9669700c 168 x = new D.Holds holds: [ arguments[0] ]
8ebc3fff
SC
169 od.$.triggerHandler 'od.hold.update', x
170 if /\/checkouts/.test url
9669700c 171 x = new D.Checkouts checkouts: [ arguments[0] ]
8ebc3fff
SC
172 od.$.triggerHandler 'od.checkout.update', x
173
174 # For a delete method, we do not get a data object in reply,
9669700c 175 # thus we pattern match for the specific ID, and trigger an
8ebc3fff
SC
176 # event with the ID.
177 if method is 'delete'
178 if id = url.match /\/holds\/(.+)\/suspension$/
179 return # no relevant event
180 if id = url.match /\/holds\/(.+)$/
9669700c 181 od.$.triggerHandler 'od.hold.delete', id[1]
8ebc3fff 182 if id = url.match /\/checkouts\/(.+)$/
9669700c 183 od.$.triggerHandler 'od.checkout.delete', id[1]
8ebc3fff
SC
184
185 .fail ->
186 od.$.triggerHandler 'od.error', [url, arguments[0]]
187 #$('<div>')._notify arguments[1].statusText, arguments[0].responseText
188
9669700c
SC
189 # Get a library access token so that we can use the Discovery API.
190 # The token is cached and also published to other modules.
8ebc3fff
SC
191 apiDiscAccess: ->
192
193 ok = (x) ->
bf67304d 194 session.token.update x
8ebc3fff 195 od.$.triggerHandler 'od.clientaccess', x
9669700c 196 return x
8ebc3fff
SC
197
198 _api session.links.token.href, grant_type: 'client_credentials'
199
200 .then ok, logError
201
202 # Use the Library Account API to get library account information,
203 # primarily the product link and the available formats. Since we
204 # schedule this call on every page load, it will also tell us if
205 # our access token has expired or not.
206 #
207 # If a retry is needed, we have to decide whether to get a library
208 # access token or a patron access token. However, getting the latter
209 # will, in the general case, require user credentials, which means we
210 # need to store the password in the browser across sessions. An
211 # alternative is to force a logout, so that the user needs to manually
212 # relogin. In effect, we would only proceed with a retry to get a
213 # library access token, but if the user has logged in, we would not.
214 #
9669700c 215 apiLibraryInfo: ->
8ebc3fff 216
6f17ad76
JD
217 get = ->
218 if session.token.token_type?
219 od.api session.links.libraries.href
220 else
221 # We have no token of either kind. Let's default to getting
222 # a library access token.
223 od.apiDiscAccess()
224 .then get, logError
225 .then ok
8ebc3fff
SC
226
227 ok = (x) ->
bf67304d
SC
228 session.links.update x
229 session.labels.update x
9669700c
SC
230 od.$.triggerHandler 'od.libraryinfo', x
231 return x
8ebc3fff
SC
232
233 retry = (jqXHR) ->
234
235 # Retry if we got a 401 error code
236 if jqXHR.status is 401
237
bf67304d 238 if session.token.is_patron_access()
8ebc3fff
SC
239 # Current OD patron access token may have expired
240 od.$.triggerHandler 'od.logout', 'od'
241
242 else
243 # Renew our access token and retry the get operation
244 od.apiDiscAccess()
245 .then get, logError
246 .then ok
247
248 get().then ok, retry
249
250 # We define a two-phase sequence to get a patron access token, for example,
251 # login(credentials); do_something_involving_page_reload(); login();
252 # where credentials is an object containing username and password
253 # properties from the login form.
254 #
255 # Logging into Evergreen can proceed using either barcode or user name,
256 # but logging into Overdrive is only a natural act using barcode. In
257 # order to ensure that logging into OD with a username can proceed, we
258 # presume that EG has been logged into and, as a prelude, we get the
259 # Preferences page of the user so that we can scrape out the barcode
260 # value for logging into OD.
261 #
262 # The login sequence is associated with a cache that remembers the
263 # login response ('parameters') between login sessions. The epilogue to
264 # the login sequence is to use the Patron Information API to get URL
265 # links and templates that will allow the user to make further use of
266 # the Circulation API.
267 login: (credentials) ->
268
269 # Temporarily store the username and password from the login form
bf67304d 270 # into the session cache, and invalidate the session token so that
8ebc3fff
SC
271 # the final part of login sequence can complete.
272 if credentials
bf67304d
SC
273 session.creds.update credentials
274 session.token.update()
8ebc3fff
SC
275 od.$.triggerHandler 'od.login'
276 return
277
bf67304d 278 # Return a promise to a resolved deferredment if session token is still valid
8ebc3fff 279 # TODO is true if in staff client but shouldn't be
bf67304d 280 if session.token.is_patron_access()
8ebc3fff
SC
281 return $.Deferred().resolve().promise()
282
283 # Request OD service for a patron access token using credentials
284 # pulled from the patron's preferences page
285 login = (prefs) ->
286
287 # Define a function to cut the value corresponding to a label
288 # from prefs
289 x = (label) ->
290 r = new RegExp "#{label}<\\/td>\\s+<td.+>(.*?)<\\/td>", 'i'
291 prefs.match(r)?[1] or ''
292
293 # Retrieve values from preferences page and save them in the
294 # session cache for later reference
bf67304d 295 session.prefs.update
8ebc3fff
SC
296 barcode: x 'barcode'
297 email_address: x 'email address'
298 home_library: x 'home library'
8ebc3fff
SC
299
300 # Use barcode as username or the username that was stored in
301 # session cache (in the hope that is a barcode) or give up with
302 # a null string
bf67304d 303 un = session.prefs.barcode or session.creds.un()
8ebc3fff
SC
304
305 # Use the password that was stored in session cache or a dummy value
27e91a35 306 pw = session.creds.pw config.password_required(session.prefs.home_library)
8ebc3fff
SC
307
308 # Remove the stored credentials from cache as soon as they are
309 # no longer needed
bf67304d 310 session.creds.update()
8ebc3fff
SC
311
312 # Determine the Open Auth scope by mapping the long name of EG
313 # home library to OD authorization name
314 scope = "websiteid:#{config.websiteID} authorizationname:#{config.authorizationname session.prefs.home_library}"
315
316 # Try to get a patron access token from OD server
317 _api session.links.patrontoken.href,
318 grant_type: 'password'
319 username: un
320 password: pw
27e91a35 321 password_required: config.password_required session.prefs.home_library
8ebc3fff
SC
322 scope: scope
323
324 # Complete login sequence if the session cache is invalid
325 ok = (x) ->
bf67304d 326 session.token.update x
8ebc3fff 327 od.$.triggerHandler 'od.patronaccess', x
9669700c 328 return x
8ebc3fff
SC
329
330 # Get patron preferences page
331 $.get '/eg/opac/myopac/prefs'
332 # Get the access token using credentials from preferences
333 .then login
334 # Update the session cache with access token
335 .then ok
336 # Update the session cache with session links
337 .then od.apiPatronInfo
338 .fail log
339
340 # TODO not used; EG catalogue is used instead
341 apiSearch: (x) ->
342 return unless x
343 od.api session.links.products.href, get, x
344
8ebc3fff
SC
345 apiMetadata: (x) ->
346 return unless x.id
9669700c 347
8ebc3fff
SC
348 od.api "#{session.links.products.href}/#{x.id}/metadata"
349
350 .then (y) ->
9669700c 351 y = new D.Metadata y
8ebc3fff
SC
352 od.$.triggerHandler 'od.metadata', y
353 y
354
355 .fail -> od.$.triggerHandler 'od.metadata', x
356
357 apiAvailability: (x) ->
358 return unless x.id
359
360 url =
361 if (alink = session.links.availability?.href)
37b7c91b 362 alink.replace '{reserveId}', x.id # Use this link if logged in
8ebc3fff
SC
363 else
364 "#{session.links.products.href}/#{x.id}/availability"
365
366 od.api url
367
8ebc3fff 368 .then (y) ->
9669700c 369 y = new D.Availability y, session.prefs.email_address
8ebc3fff 370 od.$.triggerHandler 'od.availability', y
9669700c 371 return y
8ebc3fff
SC
372
373 .fail -> od.$.triggerHandler 'od.availability', x
374
375 apiPatronInfo: ->
9669700c 376
8ebc3fff 377 ok = (x) ->
bf67304d 378 session.links.update x
8ebc3fff 379 od.$.triggerHandler 'od.patroninfo', x
9669700c 380 return x
8ebc3fff
SC
381
382 od.api session.links.patrons.href
383 .then ok, logError
384
9669700c 385 # Get a specific hold or all holds
8ebc3fff 386 apiHoldsGet: (x) ->
bf67304d 387 return unless session.token.is_patron_access()
8ebc3fff
SC
388
389 od.api "#{session.links.holds.href}#{if x?.productID then x.productID else ''}"
390
8ebc3fff 391 .then (y) ->
9669700c 392 y = new D.Holds y
8ebc3fff 393 od.$.triggerHandler 'od.holds', y
9669700c 394 return y
8ebc3fff 395
9669700c 396 # Get a specific checkout or all checkouts
8ebc3fff 397 apiCheckoutsGet: (x) ->
bf67304d 398 return unless session.token.is_patron_access()
8ebc3fff
SC
399
400 od.api "#{session.links.checkouts.href}#{if x?.reserveID then x.reserveID else ''}"
401
8ebc3fff 402 .then (y) ->
9669700c 403 y = new D.Checkouts y
8ebc3fff 404 od.$.triggerHandler 'od.checkouts', y
9669700c 405 return y
8ebc3fff 406
9669700c
SC
407 # Consolidate the holds and checkouts lists into an object that
408 # represents the 'interests' of the patron
8ebc3fff
SC
409 apiInterestsGet: ->
410 $.when(
411 od.apiHoldsGet()
412 od.apiCheckoutsGet()
413 )
8ebc3fff 414 .then (h, c) ->
9669700c
SC
415 y = new D.Interests h, c
416 od.$.triggerHandler 'od.interests', y
417 return y
8ebc3fff
SC
418
419 return od