Merge remote-tracking branch 'origin/zegooglesdosomething'

This commit is contained in:
Jake Moshenko 2014-09-04 20:10:16 -04:00
commit 3c57e612b3
14 changed files with 350 additions and 96 deletions

View file

@ -19,7 +19,7 @@ def build_requests_session():
CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID', CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'GITHUB_CLIENT_ID',
'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY', 'GITHUB_LOGIN_CLIENT_ID', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY',
'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE',
'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT'] 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'GOOGLE_LOGIN_CLIENT_ID']
def getFrontendVisibleConfig(config_dict): def getFrontendVisibleConfig(config_dict):
@ -115,6 +115,13 @@ class DefaultConfig(object):
GITHUB_LOGIN_CLIENT_ID = '' GITHUB_LOGIN_CLIENT_ID = ''
GITHUB_LOGIN_CLIENT_SECRET = '' GITHUB_LOGIN_CLIENT_SECRET = ''
# Google Config.
GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
GOOGLE_USER_URL = 'https://www.googleapis.com/oauth2/v1/userinfo'
GOOGLE_LOGIN_CLIENT_ID = ''
GOOGLE_LOGIN_CLIENT_SECRET = ''
# Requests based HTTP client with a large request pool # Requests based HTTP client with a large request pool
HTTPCLIENT = build_requests_session() HTTPCLIENT = build_requests_session()
@ -144,6 +151,9 @@ class DefaultConfig(object):
# Feature Flag: Whether GitHub login is supported. # Feature Flag: Whether GitHub login is supported.
FEATURE_GITHUB_LOGIN = False FEATURE_GITHUB_LOGIN = False
# Feature Flag: Whether Google login is supported.
FEATURE_GOOGLE_LOGIN = False
# Feature flag, whether to enable olark chat # Feature flag, whether to enable olark chat
FEATURE_OLARK_CHAT = False FEATURE_OLARK_CHAT = False

View file

@ -120,6 +120,7 @@ class FederatedLogin(BaseModel):
user = ForeignKeyField(User, index=True) user = ForeignKeyField(User, index=True)
service = ForeignKeyField(LoginService, index=True) service = ForeignKeyField(LoginService, index=True)
service_ident = CharField() service_ident = CharField()
metadata_json = TextField(default='{}')
class Meta: class Meta:
database = db database = db

View file

@ -0,0 +1,43 @@
"""add metadata field to external logins
Revision ID: 1594a74a74ca
Revises: f42b0ea7a4d
Create Date: 2014-09-04 18:17:35.205698
"""
# revision identifiers, used by Alembic.
revision = '1594a74a74ca'
down_revision = 'f42b0ea7a4d'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from data.model.sqlalchemybridge import gen_sqlalchemy_metadata
from data.database import all_models
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('federatedlogin', sa.Column('metadata_json', sa.Text(), nullable=False))
### end Alembic commands ###
schema = gen_sqlalchemy_metadata(all_models)
op.bulk_insert(schema.tables['loginservice'],
[
{'id':4, 'name':'google'},
])
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('federatedlogin', 'metadata_json')
### end Alembic commands ###
schema = gen_sqlalchemy_metadata(all_models)
loginservice = schema.table['loginservice']
op.execute(
(loginservice.delete()
.where(loginservice.c.name == op.inline_literal('google')))
)

View file

@ -387,7 +387,8 @@ def set_team_org_permission(team, team_role_name, set_by_username):
return team return team
def create_federated_user(username, email, service_name, service_id, set_password_notification): def create_federated_user(username, email, service_name, service_id,
set_password_notification, metadata={}):
if not is_create_user_allowed(): if not is_create_user_allowed():
raise TooManyUsersException() raise TooManyUsersException()
@ -397,7 +398,8 @@ def create_federated_user(username, email, service_name, service_id, set_passwor
service = LoginService.get(LoginService.name == service_name) service = LoginService.get(LoginService.name == service_name)
FederatedLogin.create(user=new_user, service=service, FederatedLogin.create(user=new_user, service=service,
service_ident=service_id) service_ident=service_id,
metadata_json=json.dumps(metadata))
if set_password_notification: if set_password_notification:
create_notification('password_required', new_user) create_notification('password_required', new_user)
@ -405,9 +407,10 @@ def create_federated_user(username, email, service_name, service_id, set_passwor
return new_user return new_user
def attach_federated_login(user, service_name, service_id): def attach_federated_login(user, service_name, service_id, metadata={}):
service = LoginService.get(LoginService.name == service_name) service = LoginService.get(LoginService.name == service_name)
FederatedLogin.create(user=user, service=service, service_ident=service_id) FederatedLogin.create(user=user, service=service, service_ident=service_id,
metadata_json=json.dumps(metadata))
return user return user
@ -426,7 +429,7 @@ def verify_federated_login(service_name, service_id):
def list_federated_logins(user): def list_federated_logins(user):
selected = FederatedLogin.select(FederatedLogin.service_ident, selected = FederatedLogin.select(FederatedLogin.service_ident,
LoginService.name) LoginService.name, FederatedLogin.metadata_json)
joined = selected.join(LoginService) joined = selected.join(LoginService)
return joined.where(LoginService.name != 'quayrobot', return joined.where(LoginService.name != 'quayrobot',
FederatedLogin.user == user) FederatedLogin.user == user)

View file

@ -40,9 +40,15 @@ def user_view(user):
organizations = model.get_user_organizations(user.username) organizations = model.get_user_organizations(user.username)
def login_view(login): def login_view(login):
try:
metadata = json.loads(login.metadata_json)
except:
metadata = {}
return { return {
'service': login.service.name, 'service': login.service.name,
'service_identifier': login.service_ident, 'service_identifier': login.service_ident,
'metadata': metadata
} }
logins = model.list_federated_logins(user) logins = model.list_federated_logins(user)
@ -89,6 +95,7 @@ class User(ApiResource):
""" Operations related to users. """ """ Operations related to users. """
schemas = { schemas = {
'NewUser': { 'NewUser': {
'id': 'NewUser', 'id': 'NewUser',
'type': 'object', 'type': 'object',
'description': 'Fields which must be specified for a new user.', 'description': 'Fields which must be specified for a new user.',

View file

@ -4,12 +4,14 @@ from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user from flask.ext.login import current_user
from endpoints.common import render_page_template, common_login, route_show_if from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics from app import app, analytics, get_app_url
from data import model from data import model
from util.names import parse_repository_name from util.names import parse_repository_name
from util.validation import generate_valid_usernames
from util.http import abort from util.http import abort
from auth.permissions import AdministerRepositoryPermission from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login from auth.auth import require_session_login
from peewee import IntegrityError
import features import features
@ -20,20 +22,39 @@ client = app.config['HTTPCLIENT']
callback = Blueprint('callback', __name__) callback = Blueprint('callback', __name__)
def render_ologin_error(service_name,
error_message='Could not load user data. The token may have expired.'):
return render_page_template('ologinerror.html', service_name=service_name,
error_message=error_message,
service_url=get_app_url())
def exchange_github_code_for_token(code, for_login=True): def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False,
redirect_suffix=''):
code = request.args.get('code') code = request.args.get('code')
id_config = service_name + '_LOGIN_CLIENT_ID' if for_login else service_name + '_CLIENT_ID'
secret_config = service_name + '_LOGIN_CLIENT_SECRET' if for_login else service_name + '_CLIENT_SECRET'
payload = { payload = {
'client_id': app.config['GITHUB_LOGIN_CLIENT_ID' if for_login else 'GITHUB_CLIENT_ID'], 'client_id': app.config[id_config],
'client_secret': app.config['GITHUB_LOGIN_CLIENT_SECRET' if for_login else 'GITHUB_CLIENT_SECRET'], 'client_secret': app.config[secret_config],
'code': code, 'code': code,
'grant_type': 'authorization_code',
'redirect_uri': '%s://%s/oauth2/%s/callback%s' % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
service_name.lower(),
redirect_suffix)
} }
headers = { headers = {
'Accept': 'application/json' 'Accept': 'application/json'
} }
get_access_token = client.post(app.config['GITHUB_TOKEN_URL'], if form_encode:
params=payload, headers=headers) get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
data=payload, headers=headers)
else:
get_access_token = client.post(app.config[service_name + '_TOKEN_URL'],
params=payload, headers=headers)
json_data = get_access_token.json() json_data = get_access_token.json()
if not json_data: if not json_data:
@ -52,17 +73,82 @@ def get_github_user(token):
return get_user.json() return get_user.json()
def get_google_user(token):
token_param = {
'access_token': token,
'alt': 'json',
}
get_user = client.get(app.config['GOOGLE_USER_URL'], params=token_param)
return get_user.json()
def conduct_oauth_login(service_name, user_id, username, email, metadata={}):
to_login = model.verify_federated_login(service_name.lower(), user_id)
if not to_login:
# try to create the user
try:
valid = next(generate_valid_usernames(username))
to_login = model.create_federated_user(valid, email, service_name.lower(),
user_id, set_password_notification=True,
metadata=metadata)
# Success, tell analytics
analytics.track(to_login.username, 'register', {'service': service_name.lower()})
state = request.args.get('state', None)
if state:
logger.debug('Aliasing with state: %s' % state)
analytics.alias(to_login.username, state)
except model.DataModelException, ex:
return render_ologin_error(service_name, ex.message)
if common_login(to_login):
return redirect(url_for('web.index'))
return render_ologin_error(service_name)
def get_google_username(user_data):
username = user_data['email']
at = username.find('@')
if at > 0:
username = username[0:at]
return username
@callback.route('/google/callback', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
def google_oauth_callback():
error = request.args.get('error', None)
if error:
return render_ologin_error('Google', error)
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE', form_encode=True)
user_data = get_google_user(token)
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
return render_ologin_error('Google')
username = get_google_username(user_data)
metadata = {
'service_username': user_data['email']
}
return conduct_oauth_login('Google', user_data['id'], username, user_data['email'],
metadata=metadata)
@callback.route('/github/callback', methods=['GET']) @callback.route('/github/callback', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN) @route_show_if(features.GITHUB_LOGIN)
def github_oauth_callback(): def github_oauth_callback():
error = request.args.get('error', None) error = request.args.get('error', None)
if error: if error:
return render_page_template('githuberror.html', error_message=error) return render_ologin_error('GitHub', error)
token = exchange_github_code_for_token(request.args.get('code')) token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token) user_data = get_github_user(token)
if not user_data: if not user_data:
return render_page_template('githuberror.html', error_message='Could not load user data') return render_ologin_error('GitHub')
username = user_data['login'] username = user_data['login']
github_id = user_data['id'] github_id = user_data['id']
@ -84,42 +170,67 @@ def github_oauth_callback():
if user_email['primary']: if user_email['primary']:
break break
to_login = model.verify_federated_login('github', github_id) metadata = {
if not to_login: 'service_username': username
# try to create the user }
try:
to_login = model.create_federated_user(username, found_email, 'github',
github_id, set_password_notification=True)
# Success, tell analytics return conduct_oauth_login('github', github_id, username, found_email, metadata=metadata)
analytics.track(to_login.username, 'register', {'service': 'github'})
state = request.args.get('state', None)
if state:
logger.debug('Aliasing with state: %s' % state)
analytics.alias(to_login.username, state)
except model.DataModelException, ex: @callback.route('/google/callback/attach', methods=['GET'])
return render_page_template('githuberror.html', error_message=ex.message) @route_show_if(features.GOOGLE_LOGIN)
@require_session_login
def google_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE',
redirect_suffix='/attach', form_encode=True)
if common_login(to_login): user_data = get_google_user(token)
return redirect(url_for('web.index')) if not user_data or not user_data.get('id', None):
return render_ologin_error('Google')
return render_page_template('githuberror.html') google_id = user_data['id']
user_obj = current_user.db_user()
username = get_google_username(user_data)
metadata = {
'service_username': user_data['email']
}
try:
model.attach_federated_login(user_obj, 'google', google_id, metadata=metadata)
except IntegrityError:
err = 'Google account %s is already attached to a %s account' % (
username, app.config['REGISTRY_TITLE_SHORT'])
return render_ologin_error('Google', err)
return redirect(url_for('web.user'))
@callback.route('/github/callback/attach', methods=['GET']) @callback.route('/github/callback/attach', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN) @route_show_if(features.GITHUB_LOGIN)
@require_session_login @require_session_login
def github_oauth_attach(): def github_oauth_attach():
token = exchange_github_code_for_token(request.args.get('code')) token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB')
user_data = get_github_user(token) user_data = get_github_user(token)
if not user_data: if not user_data:
return render_page_template('githuberror.html', error_message='Could not load user data') return render_ologin_error('GitHub')
github_id = user_data['id'] github_id = user_data['id']
user_obj = current_user.db_user() user_obj = current_user.db_user()
model.attach_federated_login(user_obj, 'github', github_id)
username = user_data['login']
metadata = {
'service_username': username
}
try:
model.attach_federated_login(user_obj, 'github', github_id, metadata=metadata)
except IntegrityError:
err = 'Github account %s is already attached to a %s account' % (
username, app.config['REGISTRY_TITLE_SHORT'])
return render_ologin_error('GitHub', err)
return redirect(url_for('web.user')) return redirect(url_for('web.user'))
@ -130,7 +241,8 @@ def github_oauth_attach():
def attach_github_build_trigger(namespace, repository): def attach_github_build_trigger(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository) permission = AdministerRepositoryPermission(namespace, repository)
if permission.can(): if permission.can():
token = exchange_github_code_for_token(request.args.get('code'), for_login=False) token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB',
for_login=False)
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
if not repo: if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository) msg = 'Invalid repository: %s/%s' % (namespace, repository)

View file

@ -179,6 +179,8 @@ def initialize_database():
TeamRole.create(name='member') TeamRole.create(name='member')
Visibility.create(name='public') Visibility.create(name='public')
Visibility.create(name='private') Visibility.create(name='private')
LoginService.create(name='google')
LoginService.create(name='github') LoginService.create(name='github')
LoginService.create(name='quayrobot') LoginService.create(name='quayrobot')
LoginService.create(name='ldap') LoginService.create(name='ldap')

View file

@ -0,0 +1,17 @@
<span class="external-login-button-element">
<span ng-if="provider == 'github'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']" ng-click="startSignin('github')" style="margin-bottom: 10px">
<i class="fa fa-github fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with GitHub</span>
<span ng-if="action == 'attach'">Attach to GitHub Account</span>
</a>
</span>
<span ng-if="provider == 'google'">
<a href="javascript:void(0)" class="btn btn-primary btn-block" quay-require="['GOOGLE_LOGIN']" ng-click="startSignin('google')">
<i class="fa fa-google fa-lg"></i>
<span ng-if="action != 'attach'">Sign In with Google</span>
<span ng-if="action == 'attach'">Attach to Google Account</span>
</a>
</span>
</span>

View file

@ -12,15 +12,13 @@
<span ng-show="tryAgainSoon == 0"> <span ng-show="tryAgainSoon == 0">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate" quay-require="['GITHUB_LOGIN']"> <span class="social-alternate" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">
<i class="fa fa-circle"></i> <i class="fa fa-circle"></i>
<span class="inner-text">OR</span> <span class="inner-text">OR</span>
</span> </span>
<a id="github-signin-link" class="btn btn-primary btn-lg btn-block" href="javascript:void(0)" ng-click="showGithub()" <div class="external-login-button" provider="github" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
quay-require="['GITHUB_LOGIN']"> <div class="external-login-button" provider="google" redirect-url="redirectUrl" sign-in-started="markStarted()"></div>
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
</span> </span>
</form> </form>

View file

@ -18,10 +18,8 @@
<i class="fa fa-circle"></i> <i class="fa fa-circle"></i>
<span class="inner-text">OR</span> <span class="inner-text">OR</span>
</span> </span>
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}" <div class="external-login-button" provider="github"></div>
class="btn btn-primary btn-block" quay-require="['GITHUB_LOGIN']"> <div class="external-login-button" provider="google"></div>
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
</div> </div>
</form> </form>
<div ng-show="registering" style="text-align: center"> <div ng-show="registering" style="text-align: center">

View file

@ -1424,10 +1424,41 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var keyService = {} var keyService = {}
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY']; keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID']; keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID']; keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback'); keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?';
keyService['googleLoginScope'] = 'openid email';
keyService['githubLoginScope'] = 'user:email';
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
if (Config.MIXPANEL_KEY && window.mixpanel) {
if (mixpanel.get_distinct_id !== undefined) {
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
}
var client_id = keyService[service + 'LoginClientId'];
var scope = keyService[service + 'LoginScope'];
var redirect_uri = keyService[service + 'RedirectUri'];
if (action == 'attach') {
redirect_uri += '/attach';
}
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
'&redirect_uri=' + redirect_uri + state_clause;
return url;
};
return keyService; return keyService;
}]); }]);
@ -2312,6 +2343,41 @@ quayApp.directive('userSetup', function () {
}); });
quayApp.directive('externalLoginButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/external-login-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'signInStarted': '&signInStarted',
'redirectUrl': '=redirectUrl',
'provider': '@provider',
'action': '@action'
},
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
$scope.startSignin = function(service) {
$scope.signInStarted({'service': service});
var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
document.location = url;
}, 250);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('signinForm', function () { quayApp.directive('signinForm', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -2328,29 +2394,6 @@ quayApp.directive('signinForm', function () {
$scope.tryAgainSoon = 0; $scope.tryAgainSoon = 0;
$scope.tryAgainInterval = null; $scope.tryAgainInterval = null;
$scope.showGithub = function() {
if (!Features.GITHUB_LOGIN) { return; }
$scope.markStarted();
var mixpanelDistinctIdClause = '';
if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
// Save the redirect URL in a cookie so that we can redirect back after GitHub returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
var url = 'https://github.com/login/oauth/authorize?client_id=' + encodeURIComponent(KeyService.githubLoginClientId) +
'&scope=user:email' + mixpanelDistinctIdClause;
document.location = url;
}, 250);
};
$scope.markStarted = function() { $scope.markStarted = function() {
if ($scope.signInStarted != null) { if ($scope.signInStarted != null) {
$scope.signInStarted(); $scope.signInStarted();
@ -2439,15 +2482,6 @@ quayApp.directive('signupForm', function () {
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) { controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
$('.form-signup').popover(); $('.form-signup').popover();
if (Config.MIXPANEL_KEY) {
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
}
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.awaitingConfirmation = false; $scope.awaitingConfirmation = false;
$scope.registering = false; $scope.registering = false;

View file

@ -1645,13 +1645,16 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
UserService.updateUserIn($scope, function(user) { UserService.updateUserIn($scope, function(user) {
$scope.cuser = jQuery.extend({}, user); $scope.cuser = jQuery.extend({}, user);
if (Features.GITHUB_LOGIN && $scope.cuser.logins) { if ($scope.cuser.logins) {
for (var i = 0; i < $scope.cuser.logins.length; i++) { for (var i = 0; i < $scope.cuser.logins.length; i++) {
if ($scope.cuser.logins[i].service == 'github') { if ($scope.cuser.logins[i].service == 'github') {
var githubId = $scope.cuser.logins[i].service_identifier; $scope.hasGithubLogin = true;
$http.get('https://api.github.com/user/' + githubId).success(function(resp) { $scope.githubLogin = $scope.cuser.logins[i].metadata['service_username'];
$scope.githubLogin = resp.login; }
});
if ($scope.cuser.logins[i].service == 'google') {
$scope.hasGoogleLogin = true;
$scope.googleLogin = $scope.cuser.logins[i].metadata['service_username'];
} }
} }
} }
@ -1669,7 +1672,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.convertStep = 0; $scope.convertStep = 0;
$scope.org = {}; $scope.org = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri; $scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.authorizedApps = null; $scope.authorizedApps = null;
$scope.logsShown = 0; $scope.logsShown = 0;

View file

@ -33,7 +33,7 @@
<li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li> <li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github" quay-require="['GITHUB_LOGIN']">GitHub Login</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#external" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">External Logins</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
<li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan"> <li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a> <a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a>
@ -163,25 +163,52 @@
</div> </div>
</div> </div>
<!-- Github tab --> <!-- External Login tab -->
<div id="github" class="tab-pane" quay-require="['GITHUB_LOGIN']"> <div id="external" class="tab-pane" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">
<div class="loading" ng-show="!cuser"> <div class="loading" ng-show="!cuser">
<div class="quay-spinner 3x"></div> <div class="quay-spinner 3x"></div>
</div> </div>
<div class="row" ng-show="cuser">
<!-- Github -->
<div class="row" quay-show="cuser && Features.GITHUB_LOGIN">
<div class="panel"> <div class="panel">
<div class="panel-title">GitHub Login:</div> <div class="panel-title">GitHub Login:</div>
<div class="panel-body"> <div class="panel-body">
<div ng-show="githubLogin" class="lead col-md-8"> <div ng-show="hasGithubLogin && githubLogin" class="lead col-md-8">
<i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i> <i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i>
<b><a href="https://github.com/{{githubLogin}}" target="_blank">{{githubLogin}}</a></b> <b><a href="https://github.com/{{githubLogin}}" target="_blank">{{githubLogin}}</a></b>
</div> </div>
<div ng-show="!githubLogin" class="col-md-8"> <div ng-show="hasGithubLogin && !githubLogin" class="lead col-md-8">
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}&redirect_uri={{ githubRedirectUri }}/attach" class="btn btn-primary"><i class="fa fa-github fa-lg"></i> Connect with GitHub</a> <i class="fa fa-github fa-lg" style="margin-right: 6px;" data-title="GitHub" bs-tooltip="tooltip.title"></i>
Account attached to Github Account
</div>
<div ng-show="!hasGithubLogin" class="col-md-4">
<span class="external-login-button" provider="github" action="attach"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Google -->
<div class="row" quay-show="cuser && Features.GOOGLE_LOGIN">
<div class="panel">
<div class="panel-title">Google Login:</div>
<div class="panel-body">
<div ng-show="hasGoogleLogin && googleLogin" class="lead col-md-8">
<i class="fa fa-google fa-lg" style="margin-right: 6px;" data-title="Google" bs-tooltip="tooltip.title"></i>
<b>{{ googleLogin }}</b>
</div>
<div ng-show="hasGoogleLogin && !googleLogin" class="lead col-md-8">
<i class="fa fa-google fa-lg" style="margin-right: 6px;" data-title="Google" bs-tooltip="tooltip.title"></i>
Account attached to Google Account
</div>
<div ng-show="!hasGoogleLogin" class="col-md-4">
<span class="external-login-button" provider="google" action="attach"></span>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Robot accounts tab --> <!-- Robot accounts tab -->

View file

@ -1,22 +1,22 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
<title>Error Logging in with GitHub · Quay.io</title> <title>Error Logging in with {{ service_name }} · Quay.io</title>
{% endblock %} {% endblock %}
{% block body_content %} {% block body_content %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h2>There was an error logging in with GitHub.</h2> <h2>There was an error logging in with {{ service_name }}.</h2>
{% if error_message %} {% if error_message %}
<div class="alert alert-danger">{{ error_message }}</div> <div class="alert alert-danger">{{ error_message }}</div>
{% endif %} {% endif %}
<div> <div>
Please register using the <a href="/">registration form</a> to continue. Please register using the <a ng-href="{{ service_url }}/signin" target="_self">registration form</a> to continue.
You will be able to connect your github account to your Quay.io account You will be able to connect your account to your Quay.io account
in the user settings. in the user settings.
</div> </div>
</div> </div>