google-admin-sdk
Version:
node.js library that wraps the Directory and Groups APIs in the Google Admin SDK
144 lines (132 loc) • 6.08 kB
text/coffeescript
quest = require 'quest'
dotty = require 'dotty'
_ = require 'underscore'
_.str = require 'underscore.string'
retry = require 'retry'
# base google api class. provides functions for:
# static methods for:
# 1. Generating an initial oauth redirect with scopes you want
# 2. Trading in authorization codes for tokens
# class object is instantiated with oauth token info
class GoogleAPIAdminSDK
constructor: (@options) ->
throw new Error('Must initialize GoogleAPI with token info') unless @options.token
throw new Error('Must provide either a refresh token or an access token') unless @options.token.refresh or @options.token.access
# client secret needed for auto-refresh
throw new Error('If providing a refresh token, must provide client id and secret') unless not @options.token.refresh or (@options.client?.id and @options.client?.secret)
retry_options: {maxTimeout: 3 * 60 * 1000, retries: 5, randomize: true}
@retry_err: (err, resp) ->
return err if err?
# 500/502 = "backend error"
# 403/503 = rate limiting errors
# 404
# 412
if resp.statusCode in [403, 404, 412, 500, 502, 503]
return new Error "Status code #{resp.statusCode} from Google"
return null
# default response interpreters--APIs should most likely specialize these
# error_handler returns true if it found/handled an error
@error_handler: (cb) ->
(err, resp, body) ->
return cb(err) or true if err
unless 200 <= resp.statusCode < 299
return cb(
code: resp.statusCode
body: body
req: _(resp?.req or {}).chain().pick('method', 'path', '_headers').extend({body:"#{resp?.req?.res?.request?.body}"}).value()
) or true
return false
@response_handler: (cb) ->
(err, resp, body) ->
return cb err, body if err?
return cb null, {'204': 'Operation success'} if resp.statusCode is 204
return cb body, null if resp.statusCode >= 400
handle_entry = (entry) ->
obj = { id: entry.id.$t, updated: entry.updated.$t, link: entry.link, title: entry.title?.$t, feedLink: entry.gd$feedLink, who: entry.gd$who }
_(entry).chain().keys().filter((k) -> k.match /^apps\$/).each (apps_key) ->
key = apps_key.match(/^apps\$(.*)$/)[1]
if key is 'property'
obj[prop.name] = prop.value for prop in entry[apps_key]
else
obj[key] = entry[apps_key]
obj
if body?.feed?
return cb null, { id: body.feed.id, link: body.feed.link, data: _(body.feed.entry or []).map(handle_entry) }
else if body?.entry
return cb null, handle_entry(body.entry)
else if _.str.include(body?.kind, 'admin#directory')
return cb null, body
else if resp.statusCode is 200 # catch all for success
return cb null, body
else
console.log 'WARNING: unhandled body', resp.statusCode, body
return cb null, body
@request_token: (options, cb) =>
# need code, client.id, client.secret
for required in ['code', 'redirect_uri', 'client.id', 'client.secret']
if not dotty.exists(options, required)
return cb new Error("Error: '#{required}' is necessary to request a token")
options =
method: 'post'
uri: 'https://accounts.google.com/o/oauth2/token'
json: true
form:
code : options.code
redirect_uri : options.redirect_uri
client_id : options.client.id
client_secret : options.client.secret
grant_type : 'authorization_code'
operation = retry.operation @retry_options
operation.attempt =>
quest options, (err, resp, body) =>
unless operation.retry GoogleAPIAdminSDK.retry_err(err, resp)
(@response_handler(cb)) err, resp, body
request_refresh_token: (cb) =>
options =
method: 'post'
uri: 'https://accounts.google.com/o/oauth2/token'
json: true
form:
refresh_token : @options.token.refresh
client_id : @options.client.id
client_secret : @options.client.secret
grant_type : 'refresh_token'
operation = retry.operation @retry_options
operation.attempt =>
quest options, (err, resp, body) =>
return if operation.retry GoogleAPIAdminSDK.retry_err(err, resp)
@options.token.access = body.access_token if body.access_token?
@options.token.id = body.id_token if body.id_token?
console.log 'Failed to refresh Google token!' if resp.statusCode isnt 200
(GoogleAPIAdminSDK.response_handler(cb)) err, resp, body
oauth2_request: (options, refreshed_already, cb) =>
if _(refreshed_already).isFunction()
cb = refreshed_already
refreshed_already = false
refresh = () =>
@request_refresh_token (err, body) =>
return cb err, null, body if err
@oauth2_request options, true, cb
return refresh() unless @options.token.access
options.headers = {} unless options.headers
_(options.headers).extend { Authorization: "Bearer #{@options.token.access}" }
operation = retry.operation @retry_options
operation.attempt =>
quest options, (err, resp, body) =>
# 401 means invalid credentials so we should refresh our token.
# But, if we refreshed_already, then we genuinely have invalid credential problems.
if not refreshed_already and resp?.statusCode is 401 and @options.token.refresh
return refresh()
# keep retrying until there is no err and resp.statusCode is not an error code
unless operation.retry GoogleAPIAdminSDK.retry_err(err, resp)
cb err, resp, body
tokeninfo: (cb) =>
operation = retry.operation @retry_options
operation.attempt =>
quest
uri: 'https://www.googleapis.com/oauth2/v1/tokeninfo'
qs: { access_token: @options.token.access }
, (err, resp, body) =>
unless operation.retry GoogleAPIAdminSDK.retry_err(err, resp)
(GoogleAPIAdminSDK.response_handler(cb)) err, resp, body
module.exports = GoogleAPIAdminSDK