-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathgoogle-auth.js
423 lines (397 loc) · 15.5 KB
/
google-auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
/**
@todo
- contacts: handle paging (& querying) instead of returning ALL (currently 3000 max)
- once google fixes it's api, just use google plus people api to get current user's email..
@fileOverview
Handles google login
@usage
1. call init with google client id (required) and scope/permissions (optional) to initialize (only needs to be called once)
2. call login with a callback event that will be $broadcast with the google credentials for the user who logged in
//initialize google auth with client id
jrgGoogleAuth.init({'client_id':LGlobals.info.googleClientId, 'scope':'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email'});
//do actual login
var evtGoogleLogin ="evtGoogleLogin";
$scope.googleLogin =function() {
jrgGoogleAuth.login({'extraInfo':{'user_id':true, 'emails':true}, 'callback':{'evtName':evtGoogleLogin, 'args':[]} });
};
// @param {Object} googleInfo
// @param {Object} token Fields directly returned from google, with the most important being access_token (but there are others not documented here - see google's documentation for full list)
// @param {String} access_token
// @param {Object} [extraInfo]
// @param {String} [user_id]
// @param {Array} [emails] Object for each email
// @param {String} value The email address itself
// @param {String?} type ?
// @param {Boolean} primary True if this is the user's primary email address
// @param {String} [emailPrimary] User's primary email address (convenience field extracted from emails array, if exists)
$scope.$on(evtGoogleLogin, function(evt, googleInfo) {
//do stuff here
});
@toc
//public
0. init
0.25. setGoogleOpts
0.5. destroy
1. login
2. getContacts
//private
1.5. loginCallback
3. pullPrimary
*/
'use strict';
angular.module('jackrabbitsgroup.angular-google-auth', [])
.factory('jrgGoogleAuth', ['$rootScope', '$http', '$q', function ($rootScope, $http, $q) {
//public methods & properties
var self ={
/**
@toc 0.
@method init
@param {Object} params
@param {String} client_id Google client id (required for login to work)
@param {String} [scope] Space delimited string of permissions to request, i.e. "https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email https://www.google.com/m8/feeds/". Defaults to "https://www.googleapis.com/auth/plus.login" otherwise
*/
init: function(params)
{
this.setGoogleOpts(params);
},
/**
Used to set google client id as well as request permissions (scope)
@toc 0.25.
@method setGoogleOpts
@param {Object} params
@param {String} client_id Google client id (required for login to work)
@param {String} [scope] Space delimited string of permissions to request, i.e. "https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email https://www.google.com/m8/feeds/". Defaults to "https://www.googleapis.com/auth/plus.login" otherwise
@param {Array} [scopeHelp =[]] Shorthand names for scope privileges so you don't need to know the full url (they'll be mapped for you here). If BOTH scope and scopeHelp are passed in, they'll be joined (but duplicates won't be checked for so don't pass in duplicates!). Available keys are: 'login', 'email', 'contacts'
*/
setGoogleOpts: function(params) {
//extend google info (client id, scope)
var ii;
//set client id
if(params.client_id) {
googleInfo.client_id =params.client_id;
}
//set scope (appending scope and scopeHelp map together, IF one or both are passed in)
var scope ='';
if(params.scope) {
scope =params.scope;
}
else if(!params.scopeHelp) { //set to default scope if NEITHER scope nor scopeHelp are set
scope =googleInfo.scope;
}
if(params.scopeHelp) {
scope +=' '; //ensure space at end of existing list
for(ii=0; ii<params.scopeHelp.length; ii++) {
if(scopeMap[params.scopeHelp[ii]]) {
scope+=scopeMap[params.scopeHelp[ii]]+' ';
}
}
}
googleInfo.scope =scope;
},
/**
@toc 0.5.
@method destroy
*/
destroy: function(params)
{
googleInfo = {
'client_id':false,
'scope': 'https://www.googleapis.com/auth/plus.login'
};
token ={};
inited =false;
},
/**
@toc 1.
@method login
@param {Object} params
@param {Object} extraInfo List of additional info to get from google such as user id (which oddly isn't returned from google authentication)
@param {Boolean} user_id true to return user id as 'user_id' field
@param {Boolean} emails true to return emails as 'emails' field - NOTE: this requires https://www.googleapis.com/auth/userinfo.email scope to be set on init. NOTE: this currently does NOT seem to work - emails field isn't coming back from Google no matter what (tried making my email publicly visible, tried in Google oAuth playground - always blank..)
@param {Object} callback
@param {String} evtName
@param {Array} args
*/
login: function(params) {
var self1 =this;
var config ={
'scope':googleInfo.scope,
'client_id':googleInfo.client_id
//'immediate': true,
};
gapi.auth.authorize(config, function() {
var googleToken =gapi.auth.getToken();
token =googleToken; //save for later use
params.returnVals ={'token':googleToken}; //values to pass back via callback in loginCallback function
if(params.extraInfo !==undefined && params.extraInfo.user_id || params.extraInfo.emails) {
//get google user id since it's not returned with authentication for some reason..
$http.defaults.headers.common["X-Requested-With"] = undefined; //for CORS to work
var url ='https://www.googleapis.com/plus/v1/people/me' +'?access_token=' + encodeURIComponent(googleToken.access_token);
$http.get(url)
.success(function(data) {
params.returnVals.rawData =data; //pass back ALL google data too
//email doesn't seem to be returned..?? even with scope set to access it.. oauth2 playground not evening returning it, even after I changed my email to be publicly visible...
params.returnVals.extraInfo ={'user_id':data.id};
if(params.extraInfo.emails) {
params.returnVals.extraInfo.emails =false; //default
if(data.emails !==undefined) {
params.returnVals.extraInfo.emails =data.emails;
loginCallback(params);
}
else { //use contacts to get email, lol..
var promise =self1.getContacts({'emailOnly':true});
promise.then(function(data) {
//put email in people api format for consistent return
params.returnVals.extraInfo.emails =[
{
value: data.email,
type: '',
primary: true
}
];
loginCallback(params);
}, function(data) {
loginCallback(params);
});
}
}
else {
loginCallback(params);
}
})
.error(function(data) {
console.log('error retrieving Google info');
loginCallback(params);
});
}
else {
loginCallback(params);
}
//get back in angular world (since did Google calls, etc. - without this, nothing will happen!)
if(!$rootScope.$$phase) {
$rootScope.$apply();
}
});
},
/**
Get a user's google contacts (also used just to get the current user's email, which is NOT returned by google plus people api for some reason..)
@toc 2.
@method getContacts
@param {Object} opts
@param {Boolean} emailOnly true if only using this to get the current user's email (instead of actually getting contacts)
@return promise with object for data on success or error. Structure depends on if it's emailOnly or not. emailOnly just returns an object with 'email' as the key.
@param {Array} contacts For each contact, an object of:
@param {String} name
@param {String} email
@param {String} phone
//@param {String} image
*/
getContacts: function(opts) {
if(!opts) {
opts ={};
}
var deferred = $q.defer();
var googleToken =token;
//set max results //@todo - handle paging (& querying) instead of returning ALL
var maxResults =3000; //set arbitrarily large
if(opts.emailOnly) { //don't care about contacts, just want current user's email
maxResults =1;
}
$http.defaults.headers.common["X-Requested-With"] = undefined; //for CORS to work
//NOTE: this isn't well documented, but can use "alt=json" to return json instead of xml
var url ='https://www.google.com/m8/feeds/contacts/default/full' +'?access_token=' + encodeURIComponent(googleToken.access_token) +'&alt=json&max-results='+maxResults;
$http.get(url)
.success(function(data) {
if(opts.emailOnly) {
deferred.resolve({'email':data.feed.id.$t});
}
else {
/*
return data structure:
feed {Object}
entry {Array} of each contact; each is an object with fields:
gd$email {Array} of email addresses; each is an object of:
address {String} the email address
primary {String} of 'true' if the primary email address
rel ?
gd$phoneNumber {Array} of phone numbers; each is an object of:
$t {String} the number
rel ?
link {Array} of link objects, including pictures. Each item is an object of: - UPDATE - images aren't showing up - may be behind authorization but not working from the app either.. so maybe these aren't profile images??
href {String}
type {String} 'image/*' for images
rel ?
title {Object} of user name
$t {String} the actual name
*/
var ii, vals, tempVal;
/**
@property contacts Array of objects, one object of info for each contact
@type Array of objects, each has fields:
@param {String} name
@param {String} email
@param {String} phone
//@param {String} image - these may not be images? the links aren't working..
*/
var contacts =[];
for(ii =0; ii<data.feed.entry.length; ii++) {
//reset / set default vals for this contact
vals ={
'email':false,
'name':false,
'phone':false
//'image':false
};
//get email
if(data.feed.entry[ii].gd$email) {
tempVal =pullPrimary(data.feed.entry[ii].gd$email, {'valueKey':'address'});
if(tempVal) {
vals.email =tempVal;
}
}
//get phone
if(data.feed.entry[ii].gd$phoneNumber) {
tempVal =pullPrimary(data.feed.entry[ii].gd$phoneNumber, {'valueKey':'$t'});
if(tempVal) {
vals.phone =tempVal;
}
}
//get name
if(data.feed.entry[ii].title) {
vals.name =data.feed.entry[ii].title.$t;
}
/*
//get image
if(data.feed.entry[ii].link) {
tempVal =pullPrimary(data.feed.entry[ii].link, {'valueKey':'href', 'matchKey':'type', 'matchVal':'image/*'});
if(tempVal) {
vals.image =tempVal;
}
}
*/
contacts[ii] =vals;
}
deferred.resolve({'contacts':contacts});
}
})
.error(function(data) {
var msg ='error retrieving Google contacts';
console.log(msg);
deferred.reject({'msg':msg});
});
return deferred.promise;
}
};
//private methods and properties - should ONLY expose methods and properties publicly (via the 'return' object) that are supposed to be used; everything else (helper methods that aren't supposed to be called externally) should be private.
var inited =false;
var token ={}; //will store token for future use / retrieval
var googleInfo ={
'client_id':false,
'scope': 'https://www.googleapis.com/auth/plus.login'
};
/**
@property scopeMap Maps shorthand keys to the appropriate google url for that privilege
@type Object
*/
var scopeMap ={
'login': 'https://www.googleapis.com/auth/plus.login',
'email': 'https://www.googleapis.com/auth/userinfo.email',
// 'email': 'https://www.googleapis.com/auth/userinfo.email https://www.google.com/m8/feeds', //NOTE: this currently does NOT seem to work BUT contacts api DOES return email.. lol.. so use that instead?! It requires an extra http request so is a bit slower, but at least it works..
'contacts': 'https://www.google.com/m8/feeds'
};
/**
@toc 1.5.
@param {Object} params
@param {Object} returnVals values to send back via callback (passed through as is)
@param {Object} callback
@param {String} evtName
@param {Array} args
@return {Object} returned via $rootScope.$broadcast event (pubSub)
@param {Object} token Fields directly returned from google, with the most important being access_token (but there are others not documented here - see google's documentation for full list)
@param {String} access_token
@param {Object} [extraInfo]
@param {String} [user_id]
@param {Array} [emails] Object for each email
@param {String} value The email address itself
@param {String?} type ?
@param {Boolean} primary True if this is the user's primary email address
@param {String} [emailPrimary] User's primary email address (convenience field extracted from emails array, if exists)
@param {Object} [rawData] The data returned directly from Google (the user profile call)
*/
function loginCallback(params) {
var ii;
//if have emails field, pull out the primary email field and return it as it's own key (for convenience)
if(params.returnVals.extraInfo.emails && params.returnVals.extraInfo.emails.length >0) {
var retVal =pullPrimary(params.returnVals.extraInfo.emails, {'valueKey':'value'});
if(retVal) {
params.returnVals.extraInfo.emailPrimary =retVal;
}
/*
for(ii =0; ii<params.returnVals.extraInfo.emails.length; ii++) {
if(params.returnVals.extraInfo.emails[ii].primary) {
params.returnVals.extraInfo.emailPrimary =params.returnVals.extraInfo.emails[ii].value;
break;
}
}
*/
}
if(params.callback.args && params.callback.args !==undefined)
{
if(params.callback.args.length ===undefined)
params.callback.args =[params.callback.args];
}
else {
params.callback.args =[];
}
var args =params.callback.args.concat([params.returnVals]);
if(args.length ==1) {
args =args[0];
}
$rootScope.$broadcast(params.callback.evtName, args);
if(!$rootScope.$$phase) { //if not already in apply / in Angular world
$rootScope.$apply();
}
}
/**
Helper function to find the "primary" item in an array of objects (i.e. get primary email
@toc 3.
@method pullPrimary
@param {Array} items The items to iterate through and find the primary one. This should be an array of objects that has a "primary" boolean field
@param {Object} [opts]
@param {String} [valueKey ='value'] Which key in the object to extract for the primary item
@param {String} [matchKey] Used in place of primary field default match; if set, then the check will be NOT on a "primary" field but on this field matching the 'matchVal' value
@param {String} [matchVal] Paired with 'matchKey' for which array item to use (instead of matching on a boolean 'primary' field)
@return {Mixed} value of primary item
*/
function pullPrimary(items, opts) {
var ii;
var valueKey ='value'; //default
var retVal =false;
if(opts.valueKey) {
valueKey =opts.valueKey;
}
var found =false;
for(ii =0; ii<items.length; ii++) {
if(opts.matchKey !==undefined && opts.matchVal !==undefined) {
if(items[ii][opts.matchKey] !==undefined && items[ii][opts.matchKey] ==opts.matchVal) {
retVal =items[ii][valueKey];
found =true;
break;
}
}
else {
if(items[ii].primary || items[ii].primary =='true') {
retVal =items[ii][valueKey];
found =true;
break;
}
}
}
//if not found, just use the first item
if(!found && items.length >0) {
retVal =items[0][valueKey];
}
return retVal;
}
return self;
}]);