Add support for login with Google. Note that this CL is not complete

This commit is contained in:
Joseph Schorr 2014-08-11 15:47:44 -04:00
parent b9c6c4c2f2
commit 2597bcef3f
10 changed files with 231 additions and 83 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

@ -7,6 +7,7 @@ from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics from app import app, analytics
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
@ -21,19 +22,31 @@ client = app.config['HTTPCLIENT']
callback = Blueprint('callback', __name__) callback = Blueprint('callback', __name__)
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):
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' % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
service_name.lower())
} }
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 +65,76 @@ 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):
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)
# 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_page_template('ologinerror.html', service_name=service_name,
error_message=ex.message)
if common_login(to_login):
return redirect(url_for('web.index'))
return render_page_template('ologinerror.html', service_name=service_name,
error_message='Unknown error')
@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_page_template('ologinerror.html', service_name='Google', error_message=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_page_template('ologinerror.html', service_name = 'Google',
error_message='Could not load user data')
username = user_data['email']
at = username.find('@')
if at > 0:
username = username[0:at]
return conduct_oauth_login('Google', user_data['id'], username, user_data['email'])
@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_page_template('ologinerror.html', service_name = 'GitHub', error_message=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_page_template('ologinerror.html', service_name = 'GitHub',
error_message='Could not load user data')
username = user_data['login'] username = user_data['login']
github_id = user_data['id'] github_id = user_data['id']
@ -84,38 +156,34 @@ def github_oauth_callback():
if user_email['primary']: if user_email['primary']:
break break
to_login = model.verify_federated_login('github', github_id) return conduct_oauth_login('github', github_id, username, found_email)
if not to_login:
# 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
analytics.track(to_login.username, 'register', {'service': 'github'})
state = request.args.get('state', None) @callback.route('/google/callback/attach', methods=['GET'])
if state: @route_show_if(features.GOOGLE_LOGIN)
logger.debug('Aliasing with state: %s' % state) @require_session_login
analytics.alias(to_login.username, state) def google_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), service_name='GOOGLE')
user_data = get_google_user(token)
if not user_data or not user_data.get('id', None):
return render_page_template('ologinerror.html', service_name = 'Google',
error_message='Could not load user data')
except model.DataModelException, ex: google_id = user_data['id']
return render_page_template('githuberror.html', error_message=ex.message) user_obj = current_user.db_user()
model.attach_federated_login(user_obj, 'google', google_id)
if common_login(to_login): return redirect(url_for('web.user'))
return redirect(url_for('web.index'))
return render_page_template('githuberror.html')
@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_page_template('ologinerror.html', service_name = 'GitHub',
error_message='Could not load user data')
github_id = user_data['id'] github_id = user_data['id']
user_obj = current_user.db_user() user_obj = current_user.db_user()
@ -130,7 +198,7 @@ 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

@ -11,10 +11,8 @@
<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>
</form> </form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div> <div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>

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>
<p class="help-block" quay-require="['BILLING']">No credit card required.</p> <p class="help-block" quay-require="['BILLING']">No credit card required.</p>
</div> </div>
</form> </form>

View file

@ -1278,10 +1278,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;
}]); }]);
@ -2150,6 +2181,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, $location, $timeout, ApiService, KeyService, UserService, 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,
@ -2163,29 +2229,6 @@ quayApp.directive('signinForm', function () {
'signedIn': '&signedIn' 'signedIn': '&signedIn'
}, },
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) { controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
$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();
@ -2235,18 +2278,9 @@ quayApp.directive('signupForm', function () {
scope: { scope: {
}, },
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

@ -1681,6 +1681,10 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.githubLogin = resp.login; $scope.githubLogin = resp.login;
}); });
} }
if ($scope.cuser.logins[i].service == 'google') {
$scope.hasGoogleLogin = true;
}
} }
} }
}); });
@ -1697,7 +1701,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>
@ -162,12 +162,14 @@
</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">
@ -175,12 +177,28 @@
<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="!githubLogin" class="col-md-4">
<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> <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" class="lead col-md-8">
Account tied to your 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,14 +1,14 @@
{% 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>
@ -16,11 +16,11 @@
<div> <div>
Please register using the <a href="/">registration form</a> to continue. Please register using the <a href="/">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>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}