Commit 0645b7dc authored by Eric Eastwood's avatar Eric Eastwood

Add sign in with GitLab

Fix #1752
parent 2ab015fb
......@@ -90,6 +90,8 @@
"apn": {
"feedbackInterval": 60
},
"gitlaboauth": {
},
"twitteroauth": {
},
"linkedinoauth2": {
......
......@@ -98,6 +98,10 @@
"exposeInBrowserTests": true,
"exposeDataForTestingPurposes": true
},
"gitlaboauth": {
"consumer_key": "",
"consumer_secret": ""
},
"twitteroauth": {
"consumer_key": "",
"consumer_secret": ""
......
......@@ -43,6 +43,10 @@
"exposeInBrowserTests": true,
"exposeDataForTestingPurposes": true
},
"gitlaboauth": {
"consumer_key": "",
"consumer_secret": ""
},
"twitteroauth": {
"consumer_key": "",
"consumer_secret": ""
......
......@@ -16,6 +16,7 @@
* [`frame-utils`](./frame-utils): Frontend: Client utility
* [`github`](./github): Backend: GitHub API
* [`github-backend`](./github-backend): Backend: Muxer GitHub Backend
* [`gitlab-backend`](./gitlab-backend): Backend: Muxer GitLab Backend
* [`google-backend`](./google-backend): Backed - Muxer Google Backend
* [`groups`](./groups): Backend: Communities/Groups
* [`i18n`](./i18n): Universal: Internationalization
......
......@@ -7,6 +7,7 @@ var identityService = require("gitter-web-identity");
var registeredBackends = {
google: require('gitter-web-google-backend'),
github: require('gitter-web-github-backend'),
gitlab: require('gitter-web-gitlab-backend'),
twitter: require('gitter-web-twitter-backend'),
linkedin: require('gitter-web-linkedin-backend'),
// ...
......
......@@ -13,6 +13,7 @@
"dependencies": {
"bluebird": "^3.2.1",
"gitter-web-github-backend": "file:../github-backend",
"gitter-web-gitlab-backend": "file:../gitlab-backend",
"gitter-web-google-backend": "file:../google-backend",
"gitter-web-identity": "file:../identity",
"gitter-web-linkedin-backend": "file:../linkedin-backend",
......
'use strict';
var Promise = require('bluebird');
function GitLabBackend(user, identity) {
this.user = user;
this.identity = identity;
}
GitLabBackend.prototype.getEmailAddress = Promise.method(function(/*preferStoredEmail*/) {
return this.identity.email;
});
GitLabBackend.prototype.findOrgs = Promise.method(function() {
return [];
});
GitLabBackend.prototype.getProfile = Promise.method(function() {
return { provider: 'gitlab' };
});
module.exports = GitLabBackend;
{
"name": "gitter-web-gitlab-backend",
"version": "1.0.0",
"main": "lib/index.js",
"directories": {
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "mocha test/"
},
"private": true,
"dependencies": {
"bluebird": "^3.2.1"
}
}
......@@ -166,6 +166,7 @@ module.exports = {
findPrimaryIdentityForUser: Promise.method(findPrimaryIdentityForUser),
GITHUB_IDENTITY_PROVIDER: 'github',
GITLAB_IDENTITY_PROVIDER: 'gitlab',
GOOGLE_IDENTITY_PROVIDER: 'google',
TWITTER_IDENTITY_PROVIDER: 'twitter',
LINKEDIN_IDENTITY_PROVIDER: 'linkedin',
......
......@@ -43,6 +43,7 @@ function parseAndValidateInput(input) {
case 'gitter':
case identityService.GITHUB_IDENTITY_PROVIDER:
case identityService.GITLAB_IDENTITY_PROVIDER:
case identityService.TWITTER_IDENTITY_PROVIDER:
return {
type: input.type,
......@@ -70,6 +71,7 @@ function parseAndValidateInput(input) {
addUserIdentifer('gitter', 'username');
addUserIdentifer(identityService.GITHUB_IDENTITY_PROVIDER, 'githubUsername');
addUserIdentifer(identityService.GITLAB_IDENTITY_PROVIDER, 'gitlabUsername');
// TODO: this doesn't actually work in the rest if the invites code
addUserIdentifer(identityService.TWITTER_IDENTITY_PROVIDER, 'twitterUsername');
......@@ -109,6 +111,7 @@ function getAvatar(type, externalId, resolvedEmailAddress) {
}
// TODO: what about a twitter user? At least one that has signed up.
// TODO: what about a gitlab user? At least one that has signed up.
if (resolvedEmailAddress) {
return avatars.getForGravatarEmail(resolvedEmailAddress);
......
......@@ -41,6 +41,7 @@ function findExistingUser(type, externalId) {
return findExistingGitterUser(externalId);
// TODO: twitter?
// TODO: gitlab?
}
return findExistingIdentityUsername(type, externalId);
......
......@@ -53,6 +53,14 @@ describe('parseAndValidateInput', function() {
it("should return the default avatar if it isn't a github username or email address", function() {
assert.strictEqual(getAvatar('twitter', '__leroux'), 'getDefault()');
});
it('should return a gravatar for an email address', function() {
assert.strictEqual(getAvatar('gitlab', 'MadLittleMods', 'contact@ericeastwood.com'), "getForGravatarEmail('contact@ericeastwood.com')");
});
it("should return the default avatar if it isn't a github username or email address", function() {
assert.strictEqual(getAvatar('gitlab', 'MadLittleMods'), 'getDefault()');
});
});
describe('parseAndValidateInput', function() {
......@@ -131,6 +139,24 @@ describe('parseAndValidateInput', function() {
emailAddress: 'lerouxb@gmail.com'
});
});
it('should return type gitlab if gitlabUsername was passed in', function() {
var result = parseAndValidateInput({gitlabUsername: 'MadLittleMods'});
assert.deepStrictEqual(result, {
type: 'gitlab',
externalId: 'MadLittleMods',
emailAddress: undefined
});
});
it('should return the email address if one was passed in with a username', function() {
var result = parseAndValidateInput({gitlabUsername: 'MadLittleMods', email: 'contact@ericeastwood.com'});
assert.deepStrictEqual(result, {
type: 'gitlab',
externalId: 'MadLittleMods',
emailAddress: 'contact@ericeastwood.com'
});
});
});
describe('new method', function() {
......@@ -143,6 +169,15 @@ describe('parseAndValidateInput', function() {
});
});
it('should parse github invites', function() {
var invite = parseAndValidateInput({ type: 'github', externalId: 'suprememoocow' });
assert.deepEqual(invite, {
emailAddress: undefined,
externalId: "suprememoocow",
type: 'github'
});
});
it('should parse twitter invites', function() {
var invite = parseAndValidateInput({ type: 'twitter', externalId: 'suprememoocow' });
assert.deepEqual(invite, {
......@@ -152,12 +187,12 @@ describe('parseAndValidateInput', function() {
});
});
it('should parse github invites', function() {
var invite = parseAndValidateInput({ type: 'github', externalId: 'suprememoocow' });
it('should parse gitlab invites', function() {
var invite = parseAndValidateInput({ type: 'gitlab', externalId: 'MadLittleMods' });
assert.deepEqual(invite, {
emailAddress: undefined,
externalId: "suprememoocow",
type: 'github'
externalId: 'MadLittleMods',
type: 'gitlab'
});
});
......
......@@ -9,7 +9,7 @@ module.exports = {
var TroupeInviteSchema = new Schema({
troupeId: { type: ObjectId, required: true },
type: { type: String, 'enum': ['email', 'github', 'twitter'], required: true },
type: { type: String, 'enum': ['email', 'github', 'twitter', 'gitlab'], required: true },
emailAddress: { type: String, required: true },
externalId: { type: String, required: true },
userId: { type: ObjectId, required: false },
......
......@@ -8,7 +8,8 @@ describe('validate-providers', function() {
[[], true],
[['github'], true],
[['twitter'], false],
[['github', 'twitter'], false],
[['gitlab'], false],
[['github', 'twitter', 'gitlab'], false],
['', false],
[{}, false],
[undefined, false],
......@@ -30,5 +31,3 @@ describe('validate-providers', function() {
});
})
});
......@@ -27,7 +27,22 @@ fi
echo "Welcome..."
if [[ -z "${gitlaboauth__client_id}${gitlaboauth__client_secret}" ]]; then
echo ""
echo "You need to create a GitLab Application to connect with GitLab"
echo "Application Name: GitLab User Dev"
echo "Redirect URI: http://localhost:5000/login/callback"
echo ""
echo "Press ENTER to open GitLab"
read nothing
browse_to https://gitlab.com/profile/applications
echo ""
echo "Paste the Application ID below and press ENTER"
read gitlaboauth__client_id
echo "Paste the Secret below and press ENTER"
read gitlaboauth__client_secret
echo ""
fi
if [[ -z "${github__user_client_id}${github__user_client_secret}" ]]; then
echo ""
......@@ -97,6 +112,8 @@ export integrations__secret="$(generate_password)"
export github__statePassphrase="$(generate_password)"
export twitteroauth__consumer_key="${twitteroauth__consumer_key}"
export twitteroauth__consumer_secret="${twitteroauth__consumer_secret}"
export gitlaboauth__client_id="${gitlaboauth__client_id}"
export gitlaboauth__client_secret="${gitlaboauth__client_secret}"
export github__client_id="${github__client_id}"
export github__client_secret="${github__client_secret}"
export github__user_client_id="${github__user_client_id}"
......
......@@ -3,7 +3,7 @@ Font license info
## Font Awesome
Copyright (C) 2012 by Dave Gandy
Copyright (C) 2016 by Dave Gandy
Author: Dave Gandy
License: SIL ()
......
......@@ -258,6 +258,12 @@
"code": 59429,
"src": "fontawesome"
},
{
"uid": "7cca4643f1e938c673e91c0c78058ddf",
"css": "gitlab",
"code": 62102,
"src": "fontawesome"
},
{
"uid": "12af2bcbfe60f39e7a1e835639d38ad8",
"css": "settings",
......
......@@ -36,4 +36,5 @@
.icon-g-cancel:before { content: '\e822'; } /* '' */
.icon-mail:before { content: '\e823'; } /* '' */
.icon-attention-circled:before { content: '\e825'; } /* '' */
.icon-spin:before { content: '\e834'; } /* '' */
\ No newline at end of file
.icon-spin:before { content: '\e834'; } /* '' */
.icon-gitlab:before { content: '\f296'; } /* '' */
\ No newline at end of file
......@@ -36,4 +36,5 @@
.icon-g-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-spin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file
.icon-spin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-gitlab { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file
......@@ -47,4 +47,5 @@
.icon-g-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-spin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file
.icon-spin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
.icon-gitlab { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file
@font-face {
font-family: 'fontello';
src: url('../fonts/fontello/font/fontello.eot?93021818');
src: url('../fonts/fontello/font/fontello.eot?93021818#iefix') format('embedded-opentype'),
url('../fonts/fontello/font/fontello.woff2?93021818') format('woff2'),
url('../fonts/fontello/font/fontello.woff?93021818') format('woff'),
url('../fonts/fontello/font/fontello.ttf?93021818') format('truetype'),
url('../fonts/fontello/font/fontello.svg?93021818#fontello') format('svg');
src: url('../fonts/fontello/font/fontello.eot?2600305');
src: url('../fonts/fontello/font/fontello.eot?2600305#iefix') format('embedded-opentype'),
url('../fonts/fontello/font/fontello.woff2?2600305') format('woff2'),
url('../fonts/fontello/font/fontello.woff?2600305') format('woff'),
url('../fonts/fontello/font/fontello.ttf?2600305') format('truetype'),
url('../fonts/fontello/font/fontello.svg?2600305#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
......@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../fonts/fontello/font/fontello.svg?93021818#fontello') format('svg');
src: url('../fonts/fontello/font/fontello.svg?2600305#fontello') format('svg');
}
}
*/
......@@ -93,3 +93,4 @@
.icon-mail:before { content: '\e823'; } /* '' */
.icon-attention-circled:before { content: '\e825'; } /* '' */
.icon-spin:before { content: '\e834'; } /* '' */
.icon-gitlab:before { content: '\f296'; } /* '' */
......@@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?51875278');
src: url('./font/fontello.eot?51875278#iefix') format('embedded-opentype'),
url('./font/fontello.woff?51875278') format('woff'),
url('./font/fontello.ttf?51875278') format('truetype'),
url('./font/fontello.svg?51875278#fontello') format('svg');
src: url('./font/fontello.eot?71902031');
src: url('./font/fontello.eot?71902031#iefix') format('embedded-opentype'),
url('./font/fontello.woff?71902031') format('woff'),
url('./font/fontello.ttf?71902031') format('truetype'),
url('./font/fontello.svg?71902031#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
......@@ -357,6 +357,7 @@ body {
<div class="row">
<div title="Code: 0xe825" class="the-icons span3"><i class="demo-icon icon-attention-circled">&#xe825;</i> <span class="i-name">icon-attention-circled</span><span class="i-code">0xe825</span></div>
<div title="Code: 0xe834" class="the-icons span3"><i class="demo-icon icon-spin animate-spin">&#xe834;</i> <span class="i-name">icon-spin</span><span class="i-code">0xe834</span></div>
<div title="Code: 0xf296" class="the-icons span3"><i class="demo-icon icon-gitlab">&#xf296;</i> <span class="i-name">icon-gitlab</span><span class="i-code">0xf296</span></div>
</div>
</div>
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
......
This diff is collapsed.
......@@ -29,8 +29,9 @@ var InviteUserResultListItemView = Marionette.ItemView.extend({
var data = _.extend({}, this.model.toJSON());
var githubUsername = this.model.get('githubUsername');
var gitlabUsername = this.model.get('gitlabUsername');
var twitterUsername = this.model.get('twitterUsername');
var username = githubUsername || twitterUsername || this.model.get('username');
var username = githubUsername || gitlabUsername || twitterUsername || this.model.get('username');
data.vendorUsername = username;
var emailAddress = data.emailAddress;
......
......@@ -76,8 +76,9 @@ var CommunityCreationPeopleListItemView = Marionette.ItemView.extend({
var data = this.model.toJSON();
var githubUsername = data.githubUsername;
var gitlabUsername = data.gitlabUsername;
var twitterUsername = data.twitterUsername;
var username = githubUsername || twitterUsername || data.username;
var username = githubUsername || gitlabUsername || twitterUsername || data.username;
data.vendorUsername = username;
data.absoluteUri = urlJoin(clientEnv.basePath, username);
var statusStates = getStatusClassStates(data.type, data.inviteStatus, this.communityCreateModel.get('allowTweetBadger'));
......
......@@ -47,8 +47,9 @@ var CommunityCreationPeopleListItemView = Marionette.ItemView.extend({
data.allowRemove = this.options.allowRemove;
var githubUsername = data.githubUsername;
var gitlabUsername = data.gitlabUsername;
var twitterUsername = data.twitterUsername;
var username = githubUsername || twitterUsername || data.username;
var username = githubUsername || gitlabUsername || twitterUsername || data.username;
data.vendorUsername = username;
data.absoluteUri = urlJoin(clientEnv.basePath, username);
......
......@@ -38,8 +38,6 @@ var View = Marionette.ItemView.extend({
action: this.action,
source: this.source,
returnTo: this.returnTo,
// TODO: remove this and just show it anyway
showTwitter: true
}
}
});
......
<section class="login-view__section">
<div class="login-view__buttons">
<a href="/login/gitlab?action={{action}}&source={{source}}{{#if returnTo}}&returnTo={{returnTo}}{{/if}}" class="login-view__button button-gitlab--small">
<i class="icon-gitlab"></i> Sign in with GitLab
</a>
<a href="/login/github?action={{action}}&source={{source}}{{#if returnTo}}&returnTo={{returnTo}}{{/if}}" class="login-view__button button-github--small">
<i class="icon-github-circled"></i> Sign in with GitHub
</a>
{{# if showTwitter}}
<a href="/login/twitter?action={{action}}&source={{source}}{{#if returnTo}}&returnTo={{returnTo}}{{/if}}" class="login-view__button button-twitter--small">
<i class="icon-twitter"></i> Sign in with Twitter
</a>
{{/if}}
</div>
<div class="modal--default__content__fineprint login-view__agree">
......
......@@ -5,6 +5,7 @@ var identifyRoute = require('gitter-web-env').middlewares.identifyRoute;
var router = express.Router({ caseSensitive: true, mergeParams: true });
var fixMongoIdQueryParam = require('../../../web/fix-mongo-id-query-param');
var Promise = require('bluebird');
var request = Promise.promisify(require('request'));
var cdn = require('gitter-web-cdn');
var avatars = require('gitter-web-avatars');
var githubUserByUsernameVersioned = require('./github-user-by-username-versioned');
......@@ -150,6 +151,26 @@ router.get('/tw/i/:id/:filename',
return twitterByIds(id, filename);
}))
router.get('/gl/u/:username',
identifyRoute('api-private-gitlab-username'),
sendAvatar(function(req) {
var username = req.params.username;
if (!username) return null;
// Gravatar or https://gitlab.com/uploads/-/system/user/avatar/:userid/avatar.png
return request({
method: 'GET',
uri: 'https://gitlab.com/api/v4/users?username=' + username,
json: true
})
.then((res) => {
return {
url: res.body.avatar_url,
longTermCachable: false
};
});
}));
/**
* Only used in DEV. Otherwise nginx handles this route
*/
......
......@@ -72,6 +72,7 @@ Try it from the CLI:
- `linkPath`: Represents how we find the backing object given the type
- `invites`: Array of user objects. Below is a list of properties you can include on each object to invite on various platforms
- `username`: Gitter username
- `gitlabUsername`: GitLab username
- `githubUsername`: GitHub username
- `twitterUsername`: Twitter username
- `emailAddress`: Manual email address
......
'use strict';
var env = require('gitter-web-env');
var identifyRoute = env.middlewares.identifyRoute;
var passport = require('passport');
var trackLoginForProvider = require('../../web/middlewares/track-login-for-provider');
var rememberMe = require('../../web/middlewares/rememberme-middleware');
var ensureLoggedIn = require('../../web/middlewares/ensure-logged-in');
var redirectAfterLogin = require('../../web/middlewares/redirect-after-login');
var passportCallbackForStrategy = require('../../web/middlewares/passport-callback-for-strategy');
var routes = {};
routes.login = [
identifyRoute('login-gitlab'),
trackLoginForProvider('gitlab'),
passport.authorize('gitlab', { failWithError: true })
];
routes.callback = [
identifyRoute('login-callback'),
passportCallbackForStrategy('gitlab', { failWithError: true }),
ensureLoggedIn,
rememberMe.generateRememberMeTokenMiddleware,
redirectAfterLogin
];
module.exports = routes;
......@@ -14,6 +14,7 @@ var oauth2 = require('../web/oauth2');
var ensureLoggedIn = require('../web/middlewares/ensure-logged-in');
var resolveUserAvatarUrl = require('gitter-web-shared/avatars/resolve-user-avatar-srcset');
var gitlab = require('./auth-providers/gitlab');
var github = require('./auth-providers/github');
var google = require('./auth-providers/google');
var twitter = require('./auth-providers/twitter');
......@@ -88,6 +89,13 @@ router.get('/upgrade', github.upgrade);
router.get(path, github.callback);
});
// ----------------------------------------------------------
// GitLab
// ----------------------------------------------------------
router.get('/gitlab', gitlab.login);
router.get('/gitlab/callback', gitlab.callback);
// ----------------------------------------------------------
// Google
// ----------------------------------------------------------
......
......@@ -6,6 +6,7 @@ var url = require('url');
var debug = require('debug')('gitter:infra:login-required-middleware');
var validAuthProviders = {
gitlab: true,
github: true,
google: true,
linkedin: true,
......
......@@ -7,6 +7,7 @@ var userService = require('../services/user-service');
var oauthService = require('../services/oauth-service');
var githubUserStrategy = require('./strategies/github-user');
var githubUpgradeStrategy = require('./strategies/github-upgrade');
var gitlabStrategy = require('./strategies/gitlab');
// var googleStrategy = require('./strategies/google');
var twitterStrategy = require('./strategies/twitter');
// var linkedinStrategy = require('./strategies/linkedin');
......@@ -96,6 +97,7 @@ function install() {
passport.use(githubUserStrategy);
passport.use(githubUpgradeStrategy);
passport.use(gitlabStrategy);
// passport.use(googleStrategy);
passport.use(twitterStrategy);
// passport.use(linkedinStrategy);
......
"use strict";
var env = require('gitter-web-env');
var config = env.config;
var GitLabStrategy = require('passport-gitlab2');
var userService = require('../../services/user-service');
var trackSignupOrLogin = require('../track-signup-or-login');
var updateUserLocale = require('../update-user-locale');
var passportLogin = require('../passport-login');
var identityService = require('gitter-web-identity');
var callbackUrlBuilder = require('./callback-url-builder');
function gitlabOauthCallback(req, token, tokenSecret, profile, done) {
var gitlabUser = {
username: profile.username+'_gitlab',
displayName: profile.displayName,
gravatarImageUrl: profile.avatarUrl
};
var gitlabIdentity = {
provider: identityService.GITLAB_IDENTITY_PROVIDER,
providerKey: profile.id,
username: profile.username,
displayName: profile.displayName,
email: profile._json.email,
accessToken: token,
accessTokenSecret: tokenSecret.access_token,
avatar: profile.avatarUrl
};
return userService.findOrCreateUserForProvider(gitlabUser, gitlabIdentity)
.spread(function(user, isNewUser) {
trackSignupOrLogin(req, user, isNewUser, 'gitlab');
updateUserLocale(req, user);
return passportLogin(req, user);
})
.asCallback(done);
}
var gitlabStrategy = new GitLabStrategy({
clientID: config.get('gitlaboauth:client_id'),
clientSecret: config.get('gitlaboauth:client_secret'),
callbackURL: callbackUrlBuilder('gitlab'),
passReqToCallback: true
}, gitlabOauthCallback);
gitlabStrategy.name = 'gitlab';
module.exports = gitlabStrategy;
'use strict';
var providerMap = {
'gitlab': 'GitLab',
'github': 'GitHub',
'twitter': 'Twitter',
'linkedin': 'LinkedIn',
......
......@@ -114,7 +114,7 @@ describe('community-creation-main-view', function() {
});
});
describe('with non-GitHub user', function() {
describe('with Twitter user', function() {
beforeEach(generateBeforeEachCb({
user: {
providers: [
......@@ -128,4 +128,18 @@ describe('community-creation-main-view', function() {
});
});
describe('with GitLab user', function() {
beforeEach(generateBeforeEachCb({
user: {
providers: [
'gitlab'
]
}
}));
it('should not show the associated project area', function() {
assert.strictEqual(view.ui.githubProjectLink.length, 0);
});
});