Skip to content

Commit 490e74d

Browse files
GitHub OAuth implementation
1 parent fb33378 commit 490e74d

File tree

13 files changed

+369
-18
lines changed

13 files changed

+369
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ config/config.json
1010
modules/nodejs
1111
modules/stdlib
1212
modules/wget
13+
gatekeeper/config.json

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ $ gulp serve:dist
4848

4949
It will automatically open the dashboard in your browser.
5050

51+
## OAuth
52+
53+
To use GTR with GitHub OAuth you must :
54+
* Register a new application on GitHub (in Settings > Applications)
55+
* Install [Gatekeeper](https://github.com/prose/gatekeeper) and launch it
56+
* Set your "gatekeeperBaseUrl" and type in your app data (clientId and GitHub URL) in config/config.json (example in config.json.dist)
57+
58+
Then, you should see the Auth button in the upper-right corner of the app !
59+
5160
## Use
5261

5362
Use directly the page path in order to select a team.

app/index.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,36 @@
2121
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
2222
<![endif]-->
2323

24-
<ng-view></ng-view>
24+
<div ui-view=""></div>
2525

2626
<!-- build:js scripts/vendor.js -->
2727
<!-- bower:js -->
28+
<script src="bower_components/jquery/dist/jquery.js"></script>
2829
<script src="bower_components/es5-shim/es5-shim.js"></script>
2930
<script src="bower_components/angular/angular.js"></script>
30-
<script src="bower_components/angular-route/angular-route.js"></script>
3131
<script src="bower_components/json3/lib/json3.js"></script>
3232
<script src="bower_components/lodash/lodash.js"></script>
33+
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
34+
<script src="bower_components/uri.js/src/URI.js"></script>
35+
<script src="bower_components/uri.js/src/IPv6.js"></script>
36+
<script src="bower_components/uri.js/src/SecondLevelDomains.js"></script>
37+
<script src="bower_components/uri.js/src/punycode.js"></script>
38+
<script src="bower_components/uri.js/src/URITemplate.js"></script>
39+
<script src="bower_components/uri.js/src/jquery.URI.js"></script>
40+
<script src="bower_components/uri.js/src/URI.min.js"></script>
41+
<script src="bower_components/uri.js/src/jquery.URI.min.js"></script>
42+
<script src="bower_components/uri.js/src/URI.fragmentQuery.js"></script>
43+
<script src="bower_components/uri.js/src/URI.fragmentURI.js"></script>
44+
<script src="bower_components/angular-uri/angular-uri.js"></script>
3345
<!-- endbower -->
3446
<!-- endbuild -->
3547

3648
<!-- build:js({app,.tmp}) scripts/main.js -->
3749
<script src="scripts/config.js"></script>
3850
<script src="scripts/app.js"></script>
3951
<script src="scripts/services/pullFetcher.js"></script>
52+
<script src="scripts/services/authManager.js"></script>
53+
<script src="scripts/controllers/auth.js"></script>
4054
<script src="scripts/controllers/main.js"></script>
4155

4256
<!-- inject:partials -->

app/scripts/app.js

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,45 @@
22

33
angular
44
.module('gtrApp', [
5-
'ngRoute',
5+
'ui.router',
6+
'angular-uri',
67
'gtrApp.config'
7-
]).config(function ($routeProvider, config) {
8-
$routeProvider
9-
.when('/:team', {
8+
]).config(function ($stateProvider, $urlRouterProvider, config) {
9+
$stateProvider
10+
11+
.state('auth', {
12+
url: '/auth',
13+
controller: 'AuthCtrl',
14+
reloadOnSearch: false
15+
})
16+
17+
.state('main', {
18+
url: '/:team',
1019
templateUrl: 'views/main.html',
1120
controller: 'MainCtrl',
12-
resolve: {team: function($q, $route, config) {
21+
resolve: {team: function($q, $stateParams, config) {
1322
var defer = $q.defer();
14-
if (config.teams[$route.current.params.team]) {
15-
defer.resolve($route.current.params.team);
23+
if (config.teams[$stateParams.team]) {
24+
defer.resolve($stateParams.team);
1625
} else {
1726
defer.reject('Team does not exist');
1827
}
1928

2029
return defer.promise;
2130
}}
22-
})
23-
.otherwise({
24-
redirectTo: '/' + Object.keys(config.teams)[0]
2531
});
26-
});
32+
33+
$urlRouterProvider.otherwise('/' + Object.keys(config.teams)[0]);
34+
}).run(['$rootScope', '$state',
35+
function($rootScope, $state) {
36+
37+
$rootScope.$on('$stateChangeStart',
38+
function(event, toState) {
39+
if (localStorage.oauthAccessToken && 'auth' === toState.name) {
40+
// already loggedin : go to app
41+
event.preventDefault();
42+
$state.go('main');
43+
}
44+
});
45+
}
46+
]);

app/scripts/controllers/auth.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
3+
angular.module('gtrApp')
4+
.controller('AuthCtrl', function ($window, URI, authManager) {
5+
var searchArr = URI($window.location.href).search(true);
6+
if (searchArr.code) {
7+
authManager.getAccessToken(searchArr.code, searchArr.state)
8+
.then(function() {
9+
$window.location.href = '/';
10+
}, function(reason) {
11+
$window.alert('Authentication failed : ' + reason);
12+
});
13+
}
14+
});

app/scripts/controllers/main.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
'use strict';
44

55
angular.module('gtrApp')
6-
.controller('MainCtrl', function ($scope, $location, $interval, PullFetcher, config, team) {
6+
.controller('MainCtrl', function ($scope, $location, $interval, PullFetcher, authManager, config, team) {
7+
var oauthEnabled = !angular.isUndefined(config.githubOAuth);
8+
$scope.oauthEnabled = oauthEnabled;
9+
if (oauthEnabled) {
10+
authManager.authenticateTeams();
11+
$scope.loginUrls = authManager.getLoginUrls();
12+
$scope.logoutClientIds = authManager.getLogoutClientIds();
13+
}
14+
715
$scope.pulls = PullFetcher.pulls;
816
$scope.teams = config.teams;
917
$scope.team = team;
@@ -65,6 +73,12 @@ angular.module('gtrApp')
6573
return array;
6674
};
6775

76+
$scope.logout = function(clientId) {
77+
authManager.logout(clientId);
78+
$scope.loginUrls = authManager.getLoginUrls();
79+
$scope.logoutClientIds = authManager.getLogoutClientIds();
80+
};
81+
6882
$scope.$watch('team', function (team) {
6983
$location.path(team);
7084
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* global _ */
2+
3+
'use strict';
4+
5+
angular.module('gtrApp')
6+
.factory('authManager', function ($http, $q, $state, URI, config) {
7+
return {
8+
9+
getAccessTokens: function() {
10+
if (!localStorage.githubOAuthAccessTokens) {
11+
return null;
12+
}
13+
14+
return JSON.parse(localStorage.githubOAuthAccessTokens);
15+
},
16+
17+
setAccessTokens: function(accessTokens) {
18+
localStorage.githubOAuthAccessTokens = JSON.stringify(accessTokens);
19+
},
20+
21+
getLoginUrls: function() {
22+
var accessTokens = this.getAccessTokens();
23+
if (!accessTokens) {
24+
accessTokens = {};
25+
}
26+
var loginUrls = [];
27+
_.forEach(config.githubOAuth.apps, function(appConfig) {
28+
if (!accessTokens[appConfig.clientId]) {
29+
var loginUrl = appConfig.url + '/login/oauth/authorize';
30+
var loginParams = {
31+
client_id: appConfig.clientId,
32+
redirect_uri: $state.href('auth', null, {absolute: true}),
33+
scope: 'repo,read:org',
34+
state: appConfig.clientId,
35+
};
36+
37+
loginUrls.push({
38+
githubHostname: URI(appConfig.url).hostname(),
39+
loginUrl: URI(loginUrl).addSearch(loginParams).toString(),
40+
});
41+
}
42+
});
43+
44+
return loginUrls;
45+
},
46+
47+
getLogoutClientIds: function() {
48+
var accessTokens = this.getAccessTokens();
49+
if (!accessTokens) {
50+
return [];
51+
}
52+
var logoutClientIds = [];
53+
_.forEach(accessTokens, function(accessToken, clientId) {
54+
var appUrl = _.findWhere(config.githubOAuth.apps, {clientId:clientId});
55+
logoutClientIds.push({
56+
clientId:clientId,
57+
githubHostname:URI(appUrl.url).hostname()
58+
});
59+
});
60+
61+
return logoutClientIds;
62+
},
63+
64+
getAccessToken: function(code, clientId) {
65+
if(angular.isUndefined(code) && angular.isUndefined(clientId)) {
66+
return false;
67+
}
68+
69+
var authManager = this;
70+
var deferred = $q.defer();
71+
72+
var gatekeeperUrl = config.githubOAuth.gatekeeperBaseUrl + '/authenticate/' + clientId + '/' + code;
73+
$http.get(gatekeeperUrl)
74+
.success(function(data) {
75+
if (data.token) {
76+
var accessTokens = authManager.getAccessTokens();
77+
if (!accessTokens) {
78+
accessTokens = {};
79+
}
80+
accessTokens[clientId] = data.token;
81+
authManager.setAccessTokens(accessTokens);
82+
83+
deferred.resolve(data.token);
84+
} else {
85+
deferred.reject('No token found');
86+
}
87+
})
88+
.error(function(reason) {
89+
deferred.reject(reason);
90+
});
91+
92+
return deferred.promise;
93+
},
94+
95+
authenticateTeams: function() {
96+
var accessTokens = this.getAccessTokens();
97+
if (accessTokens) {
98+
// Add OAuth token on each team if found
99+
_.forEach(config.teams, function(team) {
100+
if (!team.token && team.oauthAppClientId && accessTokens[team.oauthAppClientId]) {
101+
team.token = accessTokens[team.oauthAppClientId];
102+
}
103+
});
104+
}
105+
},
106+
107+
logout: function(clientId) {
108+
if(angular.isUndefined(clientId)) {
109+
return false;
110+
}
111+
112+
var accessTokens = this.getAccessTokens();
113+
if (accessTokens) {
114+
delete accessTokens[clientId];
115+
this.setAccessTokens(accessTokens);
116+
}
117+
},
118+
119+
};
120+
});

app/styles/main.css

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ body {
66
a {
77
text-decoration: none;
88
color: inherit;
9+
cursor: pointer;
910
}
1011

1112
.header-top {
@@ -25,7 +26,7 @@ a {
2526
.header-top select {
2627
float: right;
2728
margin-right: 6px;
28-
}
29+
}
2930

3031
.header-users {
3132
background-color: #3cc0bf;
@@ -87,3 +88,74 @@ ul.pulls .repo {
8788
line-height: 0;
8889
clear: both;
8990
}
91+
92+
.popover-content {
93+
padding: 9px 14px;
94+
}
95+
96+
* {
97+
-moz-box-sizing: border-box;
98+
box-sizing: border-box;
99+
}
100+
101+
.popover-container {
102+
position: relative;
103+
}
104+
105+
.popover.bottom {
106+
margin-top: 26px;
107+
}
108+
.popover {
109+
border-color: #eee;
110+
border-bottom: 2px solid #e4eaec;
111+
border-radius: 3px;
112+
}
113+
.progress, .progress .progress-bar, .popover {
114+
box-shadow: 0 0 0 #000;
115+
}
116+
.popover {
117+
position: absolute;
118+
top: 0;
119+
left: -155px;
120+
z-index: 9;
121+
max-width: 276px;
122+
padding: 1px;
123+
text-align: left;
124+
background-color: #fff;
125+
background-clip: padding-box;
126+
border: 1px solid #e1e1e1;
127+
box-shadow: 0 5px 10px rgba(0,0,0,.2);
128+
white-space: normal;
129+
}
130+
131+
.popover.bottom>.arrow {
132+
right: 0;
133+
margin-left: -22px;
134+
border-top-width: 0;
135+
border-bottom-color: #fff;
136+
top: -11px;
137+
}
138+
.popover>.arrow {
139+
border-width: 11px;
140+
}
141+
.popover>.arrow, .popover>.arrow:after {
142+
position: absolute;
143+
display: block;
144+
width: 0;
145+
height: 0;
146+
border-color: transparent;
147+
border-style: solid;
148+
}
149+
150+
.popover li {
151+
border-bottom: 1px solid #ccc;
152+
}
153+
.popover li:last-child {
154+
border-bottom: 0;
155+
}
156+
157+
ul {
158+
list-style: none;
159+
padding: 0;
160+
margin: 0;
161+
}

app/views/main.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
<header class="header-top">
22
<a href="http://github.com/m6web/GithubTeamReviewer">Github Team Reviewer</a> by <a href="http://tech.m6web.fr">M6Web</a>
3+
<div ng-show="oauthEnabled" class="pull-right popover-container">
4+
<a class="btn btn-link" ng-click="authPopover = !authPopover">Auth</a>
5+
<div class="popover bottom" ng-show="authPopover">
6+
<div class="arrow"></div>
7+
<div class="popover-content">
8+
<ul>
9+
<li ng-repeat="login in loginUrls"><a href="{{ login.loginUrl }}">Authenticate to {{ login.githubHostname }}</a></li>
10+
<li ng-repeat="logoutData in logoutClientIds"><a ng-click="logout(logoutData.clientId)">Logout from {{ logoutData.githubHostname }}</a></li>
11+
</ul>
12+
</div>
13+
</div>
14+
</div>
315
<select ng-model="team" ng-options="teamName as teamName for (teamName, team) in teams"></select>
416
</header>
517

0 commit comments

Comments
 (0)