Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
2ae4dbd9fa
17 changed files with 300 additions and 65 deletions
|
@ -4,8 +4,7 @@ import dateutil.parser
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
from database import *
|
from database import *
|
||||||
from util.validation import (validate_email, validate_username,
|
from util.validation import *
|
||||||
validate_password)
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -15,18 +14,50 @@ class DataModelException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, password, email):
|
class InvalidEmailAddressException(DataModelException):
|
||||||
pw_hash = bcrypt.hashpw(password, bcrypt.gensalt())
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidUsernameException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPasswordException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username, password, email):
|
||||||
if not validate_email(email):
|
if not validate_email(email):
|
||||||
raise DataModelException('Invalid email address: %s' % email)
|
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||||
if not validate_username(username):
|
if not validate_username(username):
|
||||||
raise DataModelException('Invalid username: %s' % username)
|
raise InvalidUsernameException('Invalid username: %s' % username)
|
||||||
if not validate_password(password):
|
|
||||||
raise DataModelException('Invalid password, password must be at least ' +
|
# We allow password none for the federated login case.
|
||||||
'8 characters and contain no whitespace.')
|
if password is not None and not validate_password(password):
|
||||||
|
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
existing = User.get((User.username == username) | (User.email == email))
|
||||||
|
|
||||||
|
logger.debug('Existing user with same username or email.')
|
||||||
|
|
||||||
|
# A user already exists with either the same username or email
|
||||||
|
if existing.username == username:
|
||||||
|
raise InvalidUsernameException('Username has already been taken: %s' %
|
||||||
|
username)
|
||||||
|
raise InvalidEmailAddressException('Email has already been used: %s' %
|
||||||
|
email)
|
||||||
|
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# This is actually the happy path
|
||||||
|
logger.debug('Email and username are unique!')
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
pw_hash = None
|
||||||
|
if password is not None:
|
||||||
|
pw_hash = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
|
|
||||||
new_user = User.create(username=username, password_hash=pw_hash,
|
new_user = User.create(username=username, password_hash=pw_hash,
|
||||||
email=email)
|
email=email)
|
||||||
return new_user
|
return new_user
|
||||||
|
@ -35,18 +66,16 @@ def create_user(username, password, email):
|
||||||
|
|
||||||
|
|
||||||
def create_federated_user(username, email, service_name, service_id):
|
def create_federated_user(username, email, service_name, service_id):
|
||||||
try:
|
new_user = create_user(username, None, email)
|
||||||
new_user = User.create(username=username, email=email, verified=True)
|
new_user.verified = True
|
||||||
|
new_user.save()
|
||||||
|
|
||||||
service = LoginService.get(LoginService.name == service_name)
|
service = LoginService.get(LoginService.name == service_name)
|
||||||
federated_user = FederatedLogin.create(user=new_user, service=service,
|
federated_user = FederatedLogin.create(user=new_user, service=service,
|
||||||
service_ident=service_id)
|
service_ident=service_id)
|
||||||
|
|
||||||
return new_user
|
return new_user
|
||||||
|
|
||||||
except Exception as ex:
|
|
||||||
raise DataModelException(ex.message)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_federated_login(service_name, service_id):
|
def verify_federated_login(service_name, service_id):
|
||||||
selected = FederatedLogin.select(FederatedLogin, User)
|
selected = FederatedLogin.select(FederatedLogin, User)
|
||||||
with_service = selected.join(LoginService)
|
with_service = selected.join(LoginService)
|
||||||
|
@ -98,7 +127,9 @@ def verify_user(username, password):
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if bcrypt.hashpw(password, fetched.password_hash) == fetched.password_hash:
|
if (fetched.password_hash and
|
||||||
|
bcrypt.hashpw(password, fetched.password_hash) ==
|
||||||
|
fetched.password_hash):
|
||||||
return fetched
|
return fetched
|
||||||
|
|
||||||
# We weren't able to authorize the user
|
# We weren't able to authorize the user
|
||||||
|
@ -176,6 +207,9 @@ def get_matching_repositories(repo_term, username=None):
|
||||||
|
|
||||||
|
|
||||||
def change_password(user, new_password):
|
def change_password(user, new_password):
|
||||||
|
if not validate_password(new_password):
|
||||||
|
raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)
|
||||||
|
|
||||||
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
|
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
|
||||||
user.password_hash = pw_hash
|
user.password_hash = pw_hash
|
||||||
user.save()
|
user.save()
|
||||||
|
|
|
@ -51,8 +51,35 @@ def get_logged_in_user():
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'gravatar': compute_hash(user.email),
|
'gravatar': compute_hash(user.email),
|
||||||
|
'askForPassword': user.password_hash is None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route('/api/user/', methods=['PUT'])
|
||||||
|
@api_login_required
|
||||||
|
def change_user_details():
|
||||||
|
user = current_user.db_user
|
||||||
|
|
||||||
|
user_data = request.get_json();
|
||||||
|
|
||||||
|
try:
|
||||||
|
if user_data['password']:
|
||||||
|
logger.debug('Changing password for user: %s', user.username)
|
||||||
|
model.change_password(user, user_data['password'])
|
||||||
|
except model.InvalidPasswordException, ex:
|
||||||
|
error_resp = jsonify({
|
||||||
|
'message': ex.message,
|
||||||
|
})
|
||||||
|
error_resp.status_code = 400
|
||||||
|
return error_resp
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'verified': user.verified,
|
||||||
|
'anonymous': False,
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email,
|
||||||
|
'gravatar': compute_hash(user.email),
|
||||||
|
'askForPassword': user.password_hash is None,
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['POST'])
|
@app.route('/api/user/', methods=['POST'])
|
||||||
def create_user_api():
|
def create_user_api():
|
||||||
|
@ -60,7 +87,7 @@ def create_user_api():
|
||||||
existing_user = model.get_user(user_data['username'])
|
existing_user = model.get_user(user_data['username'])
|
||||||
if existing_user:
|
if existing_user:
|
||||||
error_resp = jsonify({
|
error_resp = jsonify({
|
||||||
'message': 'The username already exists'
|
'message': 'The username already exists'
|
||||||
})
|
})
|
||||||
error_resp.status_code = 400
|
error_resp.status_code = 400
|
||||||
return error_resp
|
return error_resp
|
||||||
|
@ -72,13 +99,8 @@ def create_user_api():
|
||||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||||
return make_response('Created', 201)
|
return make_response('Created', 201)
|
||||||
except model.DataModelException as ex:
|
except model.DataModelException as ex:
|
||||||
message = ex.message
|
|
||||||
m = re.search('column ([a-zA-Z]+) is not unique', message)
|
|
||||||
if m and m.group(1):
|
|
||||||
message = m.group(1) + ' already exists'
|
|
||||||
|
|
||||||
error_resp = jsonify({
|
error_resp = jsonify({
|
||||||
'message': message,
|
'message': ex.message,
|
||||||
})
|
})
|
||||||
error_resp.status_code = 400
|
error_resp.status_code = 400
|
||||||
return error_resp
|
return error_resp
|
||||||
|
|
|
@ -7,7 +7,7 @@ from flask.ext.login import login_user, UserMixin, login_required, logout_user
|
||||||
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
|
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, login_manager
|
from app import app, login_manager, mixpanel
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -34,8 +34,9 @@ def load_user(username):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'], defaults={'path': ''})
|
||||||
def index():
|
@app.route('/<path:path>') # Catch all
|
||||||
|
def index(path):
|
||||||
return send_file('templates/index.html')
|
return send_file('templates/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,14 +136,26 @@ def github_oauth_callback():
|
||||||
to_login = model.verify_federated_login('github', github_id)
|
to_login = model.verify_federated_login('github', github_id)
|
||||||
if not to_login:
|
if not to_login:
|
||||||
# try to create the user
|
# try to create the user
|
||||||
to_login = model.create_federated_user(username, found_email, 'github',
|
|
||||||
github_id)
|
try:
|
||||||
|
to_login = model.create_federated_user(username, found_email, 'github',
|
||||||
|
github_id)
|
||||||
|
except model.DataModelException, ex:
|
||||||
|
return render_template('githuberror.html', error_message=ex.message)
|
||||||
|
|
||||||
if common_login(to_login):
|
if common_login(to_login):
|
||||||
|
# Success
|
||||||
|
mixpanel.track(to_login.username, 'register', {'service': 'github'})
|
||||||
|
|
||||||
|
state = request.args.get('state', None)
|
||||||
|
if state:
|
||||||
|
logger.debug('Aliasing with state: %s' % state)
|
||||||
|
mixpanel.alias(to_login.username, state)
|
||||||
|
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
# TODO something bad happened, we need to tell the user somehow
|
# TODO something bad happened, we need to tell the user somehow
|
||||||
return redirect(url_for('signin'))
|
return render_template('githuberror.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/confirm', methods=['GET'])
|
@app.route('/confirm', methods=['GET'])
|
||||||
|
|
|
@ -72,7 +72,7 @@ if __name__ == '__main__':
|
||||||
logger.debug('Populating the DB with test data.')
|
logger.debug('Populating the DB with test data.')
|
||||||
|
|
||||||
new_user_1 = model.create_user('devtable', 'password',
|
new_user_1 = model.create_user('devtable', 'password',
|
||||||
'jake@devtable.com')
|
'jschorr@devtable.com')
|
||||||
new_user_1.verified = True
|
new_user_1.verified = True
|
||||||
new_user_1.save()
|
new_user_1.save()
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
margin-top: -20px;
|
margin-top: -20px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
|
||||||
padding-top: 66px;
|
padding-top: 46px;
|
||||||
|
|
||||||
min-height: 440px;
|
min-height: 440px;
|
||||||
}
|
}
|
||||||
|
@ -137,11 +137,36 @@
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-signup input.ng-invalid.ng-dirty {
|
.signin-buttons {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-signup-button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-social-alternate {
|
||||||
|
color: #777;
|
||||||
|
font-size: 2em;
|
||||||
|
margin-left: 43px;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-social-alternate .inner-text {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
left: -43px;
|
||||||
|
top: -9px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input.ng-invalid.ng-dirty {
|
||||||
background-color: #FDD7D9;
|
background-color: #FDD7D9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-signup input.ng-valid.ng-dirty {
|
form input.ng-valid.ng-dirty {
|
||||||
background-color: #DDFFEE;
|
background-color: #DDFFEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +226,7 @@
|
||||||
@media (max-height: 768px) {
|
@media (max-height: 768px) {
|
||||||
.landing {
|
.landing {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 46px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,6 +712,11 @@ p.editable:hover i {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-admin .form-change-pw input {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
#image-history-container .node circle {
|
#image-history-container .node circle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
|
|
|
@ -5,7 +5,8 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
verified: false,
|
verified: false,
|
||||||
anonymous: true,
|
anonymous: true,
|
||||||
username: null,
|
username: null,
|
||||||
email: null
|
email: null,
|
||||||
|
askForPassword: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var userService = {}
|
var userService = {}
|
||||||
|
@ -22,6 +23,9 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
'$username': userResponse.username,
|
'$username': userResponse.username,
|
||||||
'verified': userResponse.verified
|
'verified': userResponse.verified
|
||||||
});
|
});
|
||||||
|
mixpanel.people.set_once({
|
||||||
|
'$created': new Date()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -41,8 +45,10 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
if ($location.host() === 'quay.io') {
|
if ($location.host() === 'quay.io') {
|
||||||
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
|
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
|
||||||
|
keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
|
||||||
} else {
|
} else {
|
||||||
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
|
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
|
||||||
|
keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyService;
|
return keyService;
|
||||||
|
@ -116,6 +122,8 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
$analyticsProvider.virtualPageviews(true);
|
$analyticsProvider.virtualPageviews(true);
|
||||||
|
|
||||||
|
$locationProvider.html5Mode(true);
|
||||||
|
|
||||||
$routeProvider.
|
$routeProvider.
|
||||||
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||||
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||||
|
|
|
@ -130,7 +130,7 @@ function RepoListCtrl($scope, Restangular, UserService) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function LandingCtrl($scope, $timeout, Restangular, UserService) {
|
function LandingCtrl($scope, $timeout, Restangular, UserService, KeyService) {
|
||||||
$('.form-signup').popover();
|
$('.form-signup').popover();
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
@ -141,6 +141,13 @@ function LandingCtrl($scope, $timeout, Restangular, UserService) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
|
||||||
|
var mixpanelId = loadedMixpanel.get_distinct_id();
|
||||||
|
$scope.github_state_clause = '&state=' + mixpanelId;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.githubClientId = KeyService.githubClientId;
|
||||||
|
|
||||||
$scope.awaitingConfirmation = false;
|
$scope.awaitingConfirmation = false;
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
|
|
||||||
|
@ -162,12 +169,6 @@ function LandingCtrl($scope, $timeout, Restangular, UserService) {
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
|
|
||||||
mixpanel.alias($scope.newUser.username);
|
mixpanel.alias($scope.newUser.username);
|
||||||
mixpanel.people.set_once({
|
|
||||||
'$email': $scope.newUser.email,
|
|
||||||
'$username': $scope.newUser.username,
|
|
||||||
'$created': new Date(),
|
|
||||||
'verified': false
|
|
||||||
});
|
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
$scope.registering = false;
|
$scope.registering = false;
|
||||||
$scope.registerError = result.data.message;
|
$scope.registerError = result.data.message;
|
||||||
|
@ -468,9 +469,13 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserAdminCtrl($scope, Restangular, PlanService, KeyService, $routeParams) {
|
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||||
$scope.plans = PlanService.planList();
|
$scope.plans = PlanService.planList();
|
||||||
|
|
||||||
|
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
|
$scope.askForPassword = currentUser.askForPassword;
|
||||||
|
}, true);
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
var subscribedToPlan = function(sub) {
|
||||||
$scope.subscription = sub;
|
$scope.subscription = sub;
|
||||||
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
|
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
|
||||||
|
@ -557,4 +562,33 @@ function UserAdminCtrl($scope, Restangular, PlanService, KeyService, $routeParam
|
||||||
$scope.subscribe(requested);
|
$scope.subscribe(requested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.updatingUser = false;
|
||||||
|
$scope.changePasswordSuccess = false;
|
||||||
|
$('.form-change-pw').popover();
|
||||||
|
|
||||||
|
$scope.changePassword = function() {
|
||||||
|
$('.form-change-pw').popover('hide');
|
||||||
|
$scope.updatingUser = true;
|
||||||
|
$scope.changePasswordSuccess = false;
|
||||||
|
var changePasswordPost = Restangular.one('user/');
|
||||||
|
changePasswordPost.customPUT($scope.user).then(function() {
|
||||||
|
$scope.updatingUser = false;
|
||||||
|
$scope.changePasswordSuccess = true;
|
||||||
|
|
||||||
|
// Reset the form
|
||||||
|
$scope.user.password = '';
|
||||||
|
$scope.user.repeatPassword = '';
|
||||||
|
$scope.changePasswordForm.$setPristine();
|
||||||
|
|
||||||
|
UserService.load();
|
||||||
|
}, function(result) {
|
||||||
|
$scope.updatingUser = false;
|
||||||
|
|
||||||
|
$scope.changePasswordError = result.data.message;
|
||||||
|
$timeout(function() {
|
||||||
|
$('.form-change-pw').popover('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
<div ng-show="user.anonymous">
|
<div ng-show="user.anonymous">
|
||||||
<h1>Secure hosting for <b>private</b> docker repositories</h1>
|
<h1>Secure hosting for <b>private</b> docker repositories</h1>
|
||||||
<h3>Use the docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
<h3>Use the docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
||||||
<div class="sellcall"><a href="#/plans">Private repository plans starting at $7/mo</a></div>
|
<div class="sellcall"><a href="/plans">Private repository plans starting at $7/mo</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="!user.anonymous">
|
<div ng-show="!user.anonymous">
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<h2>Your Top Repositories</h2>
|
<h2>Your Top Repositories</h2>
|
||||||
<div class="repo-listing" ng-repeat="repository in myrepos">
|
<div class="repo-listing" ng-repeat="repository in myrepos">
|
||||||
<i class="icon-hdd icon-large"></i>
|
<i class="icon-hdd icon-large"></i>
|
||||||
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,9 +25,9 @@
|
||||||
You don't have any <b>private</b> repositories yet!
|
You don't have any <b>private</b> repositories yet!
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<div class="option"><a href="#/guide">Learn how to create a repository</a></div>
|
<div class="option"><a href="/guide">Learn how to create a repository</a></div>
|
||||||
<div class="or"><span>or</span></div>
|
<div class="or"><span>or</span></div>
|
||||||
<div class="option"><a href="#/repository">Browse the public repositories</a></div>
|
<div class="option"><a href="/repository">Browse the public repositories</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,9 +40,14 @@
|
||||||
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required>
|
<input type="text" class="form-control" placeholder="Create a username" name="username" ng-model="newUser.username" autofocus required>
|
||||||
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
<input type="email" class="form-control" placeholder="Email address" ng-model="newUser.email" required>
|
||||||
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
|
<input type="password" class="form-control" placeholder="Create a password" ng-model="newUser.password" required>
|
||||||
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatePassword" match="newUser.password" required>
|
<input type="password" class="form-control" placeholder="Verify your password" ng-model="newUser.repeatPassword" match="newUser.password" required>
|
||||||
<div class="form-group">
|
<div class="form-group signin-buttons">
|
||||||
<button class="btn btn-lg btn-primary btn-block" ng-disabled="signupForm.$invalid" type="submit" analytics-on analytics-event="register">Sign Up for Free!</button>
|
<button class="btn btn-primary btn-block landing-signup-button" ng-disabled="signupForm.$invalid" type="submit" analytics-on analytics-event="register">Sign Up for Free!</button>
|
||||||
|
<span class="landing-social-alternate">
|
||||||
|
<i class="icon-circle"></i>
|
||||||
|
<span class="inner-text">OR</span>
|
||||||
|
</span>
|
||||||
|
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}" class="btn btn-primary btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
||||||
<p class="help-block">No credit card required.</p>
|
<p class="help-block">No credit card required.</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -152,7 +157,7 @@
|
||||||
<h4>Support</h4>
|
<h4>Support</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="mailto:support@quay.io">Contact Support</a></li>
|
<li><a href="mailto:support@quay.io">Contact Support</a></li>
|
||||||
<li><a href="#/guide/">Getting Started Guide</a></li>
|
<li><a href="/guide/">Getting Started Guide</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a href="{{ '#/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="icon-chevron-left"></i></a>
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="icon-chevron-left"></i></a>
|
||||||
<h3>
|
<h3>
|
||||||
<i class="icon-hdd icon-large"></i> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
<i class="icon-hdd icon-large"></i> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div ng-show="private_repositories.length > 0">
|
<div ng-show="private_repositories.length > 0">
|
||||||
<div class="repo-listing" ng-repeat="repository in private_repositories">
|
<div class="repo-listing" ng-repeat="repository in private_repositories">
|
||||||
<i class="icon-hdd icon-large"></i>
|
<i class="icon-hdd icon-large"></i>
|
||||||
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<div ng-show="private_repositories.length == 0" style="padding:20px;">
|
<div ng-show="private_repositories.length == 0" style="padding:20px;">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<h4>You don't have any repositories yet!</h4>
|
<h4>You don't have any repositories yet!</h4>
|
||||||
<a href="#/guide"><b>Click here</b> to learn how to create a repository</a>
|
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<h3>Top Public Repositories</h3>
|
<h3>Top Public Repositories</h3>
|
||||||
<div class="repo-listing" ng-repeat="repository in public_repositories">
|
<div class="repo-listing" ng-repeat="repository in public_repositories">
|
||||||
<i class="icon-hdd icon-large"></i>
|
<i class="icon-hdd icon-large"></i>
|
||||||
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
<div class="alert alert-danger">{{ errorMessage }}</div>
|
<div class="alert alert-danger">{{ errorMessage }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" ng-show="askForPassword">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-warning">Your account does not currently have a password. You will need to create a password before you will be able to <strong>push</strong> or <strong>pull</strong> repositories.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row" ng-hide="planLoading">
|
<div class="row" ng-hide="planLoading">
|
||||||
<div class="col-md-3" ng-repeat='plan in plans'>
|
<div class="col-md-3" ng-repeat='plan in plans'>
|
||||||
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
|
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
|
||||||
|
@ -52,4 +57,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="loading" ng-show="updatingUser">
|
||||||
|
<i class="icon-spinner icon-spin icon-3x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Change Password
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form class="form-change-pw" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual" data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
||||||
|
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
|
||||||
|
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword" match="user.password" required>
|
||||||
|
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit" analytics-on analytics-event="register">Change Password</button>
|
||||||
|
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||||
|
|
||||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
|
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings">
|
||||||
<a href="{{ '#/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||||
<i class="icon-cog icon-large"></i>
|
<i class="icon-cog icon-large"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
29
templates/githuberror.html
Normal file
29
templates/githuberror.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Error Logging in with GitHub - Quay</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="static/css/signin.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>There was an error logging in with GitHub.</h2>
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<div class="alert alert-danger">{{ error_message }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Please register using the <a href="/">registration form</a> to continue.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -2,9 +2,12 @@
|
||||||
<html ng-app="quay">
|
<html ng-app="quay">
|
||||||
<head>
|
<head>
|
||||||
<title ng-bind="title + ' · Quay'">Quay - Private Docker Repository</title>
|
<title ng-bind="title + ' · Quay'">Quay - Private Docker Repository</title>
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
||||||
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
||||||
|
<meta name="fragment" content="!" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
@ -60,15 +63,15 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="#">Quay</a>
|
<a class="navbar-brand" href="/">Quay</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Collapsable stuff -->
|
<!-- Collapsable stuff -->
|
||||||
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a ng-href="#/repository/">Repositories</a></li>
|
<li><a ng-href="/repository/">Repositories</a></li>
|
||||||
<li><a ng-href="#/guide/">Getting Started Guide</a></li>
|
<li><a ng-href="/guide/">Getting Started Guide</a></li>
|
||||||
<li><a ng-href="#/plans/">Plans and Pricing</a></li>
|
<li><a ng-href="/plans/">Plans and Pricing</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,15 +88,21 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
|
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
|
<span class="badge" ng-show="user.askForPassword">1</span>
|
||||||
<b class="caret"></b>
|
<b class="caret"></b>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="#/user">Account Settings</a></li>
|
<li>
|
||||||
<li><a href="/signout">Sign out</a></li>
|
<a href="/user">
|
||||||
|
Account Settings
|
||||||
|
<span class="badge" ng-show="user.askForPassword">1</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="/signout" target="_self">Sign out</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li ng-switch-default>
|
<li ng-switch-default>
|
||||||
<a href="/signin">Sign in</a>
|
<a href="/signin" target="_self">Sign in</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- /.navbar-collapse -->
|
</div><!-- /.navbar-collapse -->
|
||||||
|
|
|
@ -7,7 +7,17 @@
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="static/css/signin.css">
|
<link rel="stylesheet" href="static/css/signin.css">
|
||||||
|
|
||||||
|
<!-- start Mixpanel --><script type="text/javascript">
|
||||||
|
var isProd = document.location.hostname === 'quay.io';
|
||||||
|
|
||||||
|
(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==
|
||||||
|
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);
|
||||||
|
b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
|
||||||
|
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false });</script><!-- end Mixpanel -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form method="post" class="form-signin">
|
<form method="post" class="form-signin">
|
||||||
|
@ -17,10 +27,10 @@
|
||||||
|
|
||||||
<span class="social-alternate">
|
<span class="social-alternate">
|
||||||
<i class="icon-circle"></i>
|
<i class="icon-circle"></i>
|
||||||
<span class="inner-text">OR</i>
|
<span class="inner-text">OR</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<a href="https://github.com/login/oauth/authorize?client_id={{ github_client_id }}&scope=user:email" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ github_client_id }}&scope=user:email" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if invalid_credentials %}
|
{% if invalid_credentials %}
|
||||||
|
@ -31,5 +41,19 @@
|
||||||
<div class="alert alert-danger">You must verify your email address before you can sign in.</div>
|
<div class="alert alert-danger">You must verify your email address before you can sign in.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
function appendMixpanelId() {
|
||||||
|
if (mixpanel.get_distinct_id !== undefined) {
|
||||||
|
var signinLink = document.getElementById("github-signin-link");
|
||||||
|
signinLink.href += ("&state=" + mixpanel.get_distinct_id());
|
||||||
|
} else {
|
||||||
|
// Mixpanel not yet loaded, try again later
|
||||||
|
window.setTimeout(appendMixpanelId, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
appendMixpanelId();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
BIN
test.db
BIN
test.db
Binary file not shown.
|
@ -1,6 +1,8 @@
|
||||||
import re
|
import re
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
INVALID_PASSWORD_MESSAGE = 'Invalid password, password must be at least ' + \
|
||||||
|
'8 characters and contain no whitespace.'
|
||||||
|
|
||||||
def validate_email(email_address):
|
def validate_email(email_address):
|
||||||
if re.match(r'[^@]+@[^@]+\.[^@]+', email_address):
|
if re.match(r'[^@]+@[^@]+\.[^@]+', email_address):
|
||||||
|
|
Reference in a new issue