Move signin to use AJAX. Render all flask templates with the common header. Move the header to a partial. Add account recovery.

This commit is contained in:
yackob03 2013-10-14 17:50:07 -04:00
parent e182163d34
commit 4c15072c5a
17 changed files with 653 additions and 617 deletions

1
app.py
View file

@ -32,7 +32,6 @@ Principal(app, use_sessions=True)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'signin'
mail = Mail()
mail.init_app(app)

View file

@ -109,6 +109,29 @@ def confirm_user_email(code):
return user
def create_reset_password_email_code(email):
try:
user = User.get(User.email == email)
except User.DoesNotExist:
raise InvalidEmailAddressException('Email address was not found.');
code = EmailConfirmation.create(user=user, pw_reset=True)
return code
def validate_reset_code(code):
try:
code = EmailConfirmation.get(EmailConfirmation.code == code,
EmailConfirmation.pw_reset == True)
except EmailConfirmation.DoesNotExist:
return None
user = code.user
code.delete_instance()
return user
def get_user(username):
try:
return User.get(User.username == username)

View file

@ -2,19 +2,21 @@ import logging
import stripe
from flask import request, make_response, jsonify, abort
from flask.ext.login import login_required, current_user
from flask.ext.login import login_required, current_user, logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from functools import wraps
from collections import defaultdict
from data import model
from app import app
from util.email import send_confirmation_email
from util.email import send_confirmation_email, send_recovery_email
from util.names import parse_repository_name
from util.gravatar import compute_hash
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission,
AdministerRepositoryPermission)
from endpoints import registry
from endpoints.web import common_login
import re
logger = logging.getLogger(__name__)
@ -106,6 +108,53 @@ def create_user_api():
return error_resp
@app.route('/api/signin', methods=['POST'])
def signin_api():
signin_data = request.get_json()
username = signin_data['username']
password = signin_data['password']
#TODO Allow email login
needs_email_verification = False
invalid_credentials = False
verified = model.verify_user(username, password)
if verified:
if common_login(verified):
return make_response('Success', 200)
else:
needs_email_verification = True
else:
invalid_credentials = True
response = jsonify({
'needsEmailVerification': needs_email_verification,
'invalidCredentials': invalid_credentials,
})
response.status_code = 403
return response
@app.route("/api/signout", methods=['POST'])
@api_login_required
def logout():
logout_user()
identity_changed.send(app, identity=AnonymousIdentity())
return make_response('Success', 200)
@app.route("/api/recovery", methods=['POST'])
def send_recovery():
email = request.get_json()['email']
code = model.create_reset_password_email_code(email)
send_recovery_email(email, code.code)
return make_response('Created', 201)
@app.route('/api/users/<prefix>', methods=['GET'])
@api_login_required
def get_matching_users(prefix):

View file

@ -1,9 +1,9 @@
import logging
import requests
from flask import (abort, send_file, redirect, request, url_for,
render_template, make_response)
from flask.ext.login import login_user, UserMixin, login_required, logout_user
from flask import (abort, redirect, request, url_for, render_template,
make_response)
from flask.ext.login import login_user, UserMixin, login_required
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
from data import model
@ -37,7 +37,7 @@ def load_user(username):
@app.route('/', methods=['GET'], defaults={'path': ''})
@app.route('/repository/<path:path>', methods=['GET'])
def index(path):
return send_file('templates/index.html')
return render_template('index.html')
@app.route('/plans/')
@ -55,6 +55,11 @@ def user():
return index('')
@app.route('/signin/')
def signin():
return index('')
@app.route('/status', methods=['GET'])
def status():
return make_response('Healthy')
@ -62,12 +67,12 @@ def status():
@app.route('/tos', methods=['GET'])
def tos():
return send_file('templates/tos.html')
return render_template('tos.html')
@app.route('/privacy', methods=['GET'])
def privacy():
return send_file('templates/privacy.html')
return render_template('privacy.html')
def common_login(db_user):
@ -81,34 +86,6 @@ def common_login(db_user):
return False
@app.route('/signin', methods=['GET'])
def render_signin_page():
return render_template('signin.html',
github_client_id=app.config['GITHUB_CLIENT_ID'])
@app.route('/signin', methods=['POST'])
def signin():
username = request.form['username']
password = request.form['password']
#TODO Allow email login
verified = model.verify_user(username, password)
if verified:
if common_login(verified):
return redirect(request.args.get('next') or url_for('index'))
else:
return render_template('signin.html',
needs_email_verification=True,
github_client_id=app.config['GITHUB_CLIENT_ID'])
else:
return render_template('signin.html',
username=username,
invalid_credentials=True,
github_client_id=app.config['GITHUB_CLIENT_ID'])
@app.route('/oauth2/github/callback', methods=['GET'])
def github_oauth_callback():
code = request.args.get('code')
@ -183,16 +160,18 @@ def confirm_email():
return redirect(url_for('index'))
@app.route('/recovery', methods=['GET'])
def confirm_recovery():
code = request.values['code']
user = model.validate_reset_code(code)
if user:
common_login(user)
return redirect(url_for('user'))
else:
abort(403)
@app.route('/reset', methods=['GET'])
def password_reset():
pass
@app.route("/signout")
@login_required
def logout():
logout_user()
identity_changed.send(app, identity=AnonymousIdentity())
return redirect(url_for('index'))

View file

@ -976,3 +976,28 @@ p.editable:hover i {
.tos ul {
margin-top: 10px;
}
.form-signin input {
margin-bottom: 20px;
}
.form-signin {
text-align: center;
margin-bottom: 20px;
}
.social-alternate {
color: #777;
font-size: 3em;
margin-left: 43px;
}
.social-alternate .inner-text {
text-align: center;
position: relative;
color: white;
left: -42px;
top: -9px;
font-weight: bold;
font-size: .4em;
}

View file

@ -1,66 +0,0 @@
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.signin-container {
text-align: center;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
text-align: center;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.alert {
max-width: 300px;
margin: 0 auto;
}
.social-alternate {
color: #777;
font-size: 3em;
margin-left: 43px;
}
.social-alternate .inner-text {
text-align: center;
position: relative;
color: white;
left: -43px;
top: -9px;
font-weight: bold;
font-size: .4em;
}

View file

@ -136,6 +136,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
when('/user/', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
when('/guide/', {title: 'Getting Started Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/signin/', {title: 'Signin', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
otherwise({redirectTo: '/'});
}]).

View file

@ -35,11 +35,21 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
function HeaderCtrl($scope, UserService) {
function HeaderCtrl($scope, $location, UserService, Restangular) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
$scope.signout = function() {
var signoutPost = Restangular.one('signout');
signoutPost.customPOST().then(function() {
UserService.load();
$location.path('/');
});
}
$scope.$on('$includeContentLoaded', function() {
// THIS IS BAD, MOVE THIS TO A DIRECTIVE
$('#repoSearch').typeahead({
name: 'repositories',
remote: {
@ -75,8 +85,51 @@ function HeaderCtrl($scope, UserService) {
$('#repoSearch').typeahead('setQuery', '');
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name
});
});
}
function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) {
$scope.githubClientId = KeyService.githubClientId;
var appendMixpanelId = function() {
if (mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id();
} else {
// Mixpanel not yet loaded, try again later
$timeout(appendMixpanelId, 200);
}
};
appendMixpanelId();
$scope.signin = function() {
var signinPost = Restangular.one('signin');
signinPost.customPOST($scope.user).then(function() {
$scope.needsEmailVerification = false;
$scope.invalidCredentials = false;
// Redirect to the landing page
UserService.load();
$location.path('/');
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
});
};
$scope.sendRecovery = function() {
var signinPost = Restangular.one('recovery');
signinPost.customPOST($scope.recovery).then(function() {
$scope.invalidEmail = false;
$scope.sent = true;
}, function(result) {
$scope.invalidEmail = true;
$scope.sent = false;
});
};
};
function PlansCtrl($scope, UserService, PlanService) {
$scope.plans = PlanService.planList();
@ -328,6 +381,8 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
$scope.$on('$viewContentLoaded', function() {
// THIS IS BAD, MOVE THIS TO A DIRECTIVE
$('#userSearch').typeahead({
name: 'users',
remote: {
@ -358,6 +413,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
$('#userSearch').typeahead('setQuery', '');
$scope.addNewPermission(datum.username);
});
});
$scope.addNewPermission = function(username) {
// Don't allow duplicates.

View file

@ -0,0 +1,53 @@
<!-- Quay -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="/static/img/quay-logo.png">
</a>
</div>
<!-- Collapsable stuff -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a ng-href="/repository/">Repositories</a></li>
<li><a ng-href="/guide/">Getting Started</a></li>
<li><a ng-href="/plans/">Plans &amp; Pricing</a></li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo">
</div>
</form>
<li class="dropdown" ng-switch-when="false">
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
<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" />
{{ user.username }}
<span class="badge" ng-show="user.askForPassword">1</span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a href="/user/">
Account Settings
<span class="badge" ng-show="user.askForPassword">1</span>
</a>
</li>
<li><a href="javascript:void(0)" ng-click="signout()">Sign out</a></li>
</ul>
</li>
<li ng-switch-default>
<a href="/signin/">Sign in</a>
</li>
</ul>
</div><!-- /.navbar-collapse -->

View file

@ -16,7 +16,7 @@
<h2>Your Top Repositories</h2>
<div class="repo-listing" ng-repeat="repository in myrepos">
<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>
</div>

View file

@ -0,0 +1,72 @@
<div class="container signin-container">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseSignin">
Sign In
</a>
</h4>
</div>
<div id="collapseSignin" class="panel-collapse collapse in">
<div class="panel-body">
<form class="form-signin" ng-submit="signin();">
<input type="text" class="form-control input-lg" placeholder="Username" ng-model="user.username" autofocus>
<input type="password" class="form-control input-lg" placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<i class="icon-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
<div class="alert alert-danger" ng-show="needsEmailVerification">You must verify your email address before you can sign in.</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h6 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseForgot">
Forgot Password
</a>
</h4>
</div>
<div id="collapseForgot" class="panel-collapse collapse out">
<div class="panel-body">
<form class="form-signin" ng-submit="sendRecovery();">
<input type="text" class="form-control input-lg" placeholder="Email" ng-model="recovery.email">
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
</form>
<div class="alert alert-danger" ng-show="invalidEmail">Unable to locate account.</div>
<div class="alert alert-success" ng-show="sent">Account recovery email was sent.</div>
</div>
</div>
</div>
</div>
</div>
</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> -->

View file

@ -1,12 +1,23 @@
<!DOCTYPE html>
<html>
<html ng-app="quay">
<head>
<title>Sign In - Quay.io</title>
{% block title %}
{% endblock %}
{% block added_meta %}
{% endblock %}
<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.">
<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="//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-theme.min.css">
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="static/css/signin.css">
<link rel="stylesheet" href="/static/css/quay.css">
<!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
@ -21,54 +32,48 @@
<link rel="apple-touch-icon" sizes="152x152" href="/static/img/apple-touch-icon-152x152.png" />
<!-- /Icons -->
<!-- start Mixpanel --><script type="text/javascript">
{% block added_stylesheets %}
{% endblock %}
<script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
<script src="static/lib/angulartics.js"></script>
<script src="static/lib/angulartics-mixpanel.js"></script>
<script src="static/lib/angular-moment.min.js"></script>
<script src="static/lib/typeahead.min.js"></script>
{% block added_dependencies %}
{% endblock %}
<script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script>
<!-- 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 -->
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel -->
</head>
<body>
<div class="container signin-container">
<img src="/static/img/quay-logo.png">
<!-- Nav bar -->
<nav class="navbar navbar-default" role="navigation" ng-include="'/static/partials/header.html'" ng-controller='HeaderCtrl' >
</nav>
<form method="post" class="form-signin">
<input type="text" class="form-control" placeholder="Username" name="username" value="{{ username }}"autofocus>
<input type="password" class="form-control" placeholder="Password" name="password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
{% block body_content %}
<span class="social-alternate">
<i class="icon-circle"></i>
<span class="inner-text">OR</span>
</span>
<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>
{% if invalid_credentials %}
<div class="alert alert-danger">Invalid username or password.</div>
{% endif %}
{% if needs_email_verification %}
<div class="alert alert-danger">You must verify your email address before you can sign in.</div>
{% endif %}
</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>
{% endblock %}
</body>
</html>

View file

@ -1,27 +1,10 @@
<!DOCTYPE html>
<html>
<head>
{% extends "base.html" %}
{% block title %}
<title>Error Logging in with GitHub · Quay.io</title>
{% endblock %}
<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">
<!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/static/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/static/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/static/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/static/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/static/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/static/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/static/img/apple-touch-icon-152x152.png" />
<!-- /Icons -->
</head>
<body>
{% block body_content %}
<div class="container">
<div class="row">
<div class="col-md-12">
@ -38,5 +21,4 @@
</div>
</div>
</body>
</html>
{% endblock %}

View file

@ -1,132 +1,37 @@
<!DOCTYPE html>
<html ng-app="quay">
<head>
{% extends "base.html" %}
{% block title %}
<title ng-bind="title + ' · Quay.io'">Quay - Private Docker Repository</title>
{% endblock %}
{% block added_meta %}
<base href="/">
<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="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
<meta name="fragment" content="!" />
{% endblock %}
<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-theme.min.css">
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
{% block added_stylesheets %}
<link rel="stylesheet" href="/static/lib/browser-chrome.css">
{% endblock %}
<link rel="stylesheet" href="/static/css/quay.css">
<!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/static/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/static/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/static/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/static/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/static/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/static/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/static/img/apple-touch-icon-152x152.png" />
<!-- /Icons -->
<script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
{% block added_dependencies %}
<script src="https://checkout.stripe.com/v2/checkout.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/zeroclipboard/1.1.7/ZeroClipboard.min.js"></script>
<script src="static/lib/angulartics.js"></script>
<script src="static/lib/angulartics-mixpanel.js"></script>
<script src="static/lib/jquery.overscroll.min.js"></script>
<script src="static/lib/angular-moment.min.js"></script>
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
<script src="static/lib/pagedown/Markdown.Editor.js"></script>
<script src="static/lib/pagedown/Markdown.Sanitizer.js"></script>
<script src="static/lib/typeahead.min.js"></script>
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
<script src="static/lib/browser-chrome.js"></script>
{% endblock %}
<script src="static/js/app.js"></script>
<script src="static/js/controllers.js"></script>
<script src="static/js/graphing.js"></script>
<!-- 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, debug: !isProd });</script><!-- end Mixpanel -->
</head>
<body>
<!-- Nav bar -->
<nav class="navbar navbar-default" role="navigation" ng-controller='HeaderCtrl'>
<!-- Quay -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="/static/img/quay-logo.png">
</a>
</div>
<!-- Collapsable stuff -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li><a ng-href="/repository/">Repositories</a></li>
<li><a ng-href="/guide/">Getting Started</a></li>
<li><a ng-href="/plans/">Plans &amp; Pricing</a></li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo">
</div>
</form>
<li class="dropdown" ng-switch-when="false">
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
<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" />
{{ user.username }}
<span class="badge" ng-show="user.askForPassword">1</span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<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>
</li>
<li ng-switch-default>
<a href="/signin" target="_self">Sign in</a>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</nav>
{% block body_content %}
<div ng-view></div>
</body>
</html>
{% endblock %}

View file

@ -1,46 +1,13 @@
<!DOCTYPE html>
<html>
<head>
{% extends "base.html" %}
{% block title %}
<title>Privacy Policy · Quay.io</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}
<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-theme.min.css">
<link rel="stylesheet" href="/static/css/quay.css">
{% block body_content %}
<div class="container privacy-policy">
<!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/static/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/static/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/static/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/static/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/static/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/static/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/static/img/apple-touch-icon-152x152.png" />
<!-- /Icons -->
</head>
<body>
<nav class="navbar navbar-default" role="navigation" ng-controller='HeaderCtrl'>
<!-- Quay -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="/static/img/quay-logo.png">
</a>
</div>
</nav>
<div class="container privacy-policy">
<h2>Privacy Policy</h2>
<h2>Privacy Policy</h2>
<dl>
<dt>What information do we collect?</dt>
@ -114,6 +81,5 @@
If you have any questions or concerns about our privacy policy, please direct them to the following email address:
<a href="mailto:support@quay.io">support@quay.io</a>
</dd>
</div>
</body>
</html>
</div>
{% endblock %}

View file

@ -1,44 +1,11 @@
<!DOCTYPE html>
<html>
<head>
{% extends "base.html" %}
{% block title %}
<title>Terms of Service · Quay.io</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}
<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-theme.min.css">
<link rel="stylesheet" href="/static/css/quay.css">
<!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/static/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/static/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/static/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/static/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/static/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/static/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/static/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/static/img/apple-touch-icon-152x152.png" />
<!-- /Icons -->
</head>
<body>
<nav class="navbar navbar-default" role="navigation" ng-controller='HeaderCtrl'>
<!-- Quay -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="/static/img/quay-logo.png">
</a>
</div>
</nav>
<div class="tos container">
{% block body_content %}
<div class="tos container">
<h2>Terms of Service</h2>
<p>The following terms and conditions govern all use of the Quay.io website and all content, services and products available at or through the website. The Website is owned and operated by DevTable, LLC. (“DevTable”). The Website is offered subject to your acceptance without modification of all of the terms and conditions contained herein and all other operating rules, policies (including, without limitation, Quay.ios Privacy Policy) and procedures that may be published from time to time on this Site by DevTable (collectively, the “Agreement”).</p>
<p>Please read this Agreement carefully before accessing or using the Website. By accessing or using any part of the web site, you agree to become bound by the terms and conditions of this agreement. If you do not agree to all the terms and conditions of this agreement, then you may not access the Website or use any services. If these terms and conditions are considered an offer by DevTable, acceptance is expressly limited to these terms. The Website is available only to individuals who are at least 13 years old.</p>
@ -127,6 +94,5 @@
<strong>Miscellaneous.</strong> This Agreement constitutes the entire agreement between DevTable and you concerning the subject matter hereof, and they may only be modified by a written amendment signed by an authorized executive of DevTable, or by the posting by DevTable of a revised version. Except to the extent applicable law, if any, provides otherwise, this Agreement, any access to or use of the Website will be governed by the laws of the state of New York, U.S.A., excluding its conflict of law provisions, and the proper venue for any disputes arising out of or relating to any of the same will be the state and federal courts located in New York County, New York. The prevailing party in any action or proceeding to enforce this Agreement shall be entitled to costs and attorneys fees. If any part of this Agreement is held invalid or unenforceable, that part will be construed to reflect the parties original intent, and the remaining portions will remain in full force and effect. A waiver by either party of any term or condition of this Agreement or any breach thereof, in any one instance, will not waive such term or condition or any subsequent breach thereof. You may assign your rights under this Agreement to any party that consents to, and agrees to be bound by, its terms and conditions; DevTable may assign its rights under this Agreement without condition. This Agreement will be binding upon and will inure to the benefit of the parties, their successors and permitted assigns.
</li>
</ol>
</div>
</body>
</html>
</div>
{% endblock %}

View file

@ -5,16 +5,37 @@ from app import mail, app
CONFIRM_MESSAGE = """
This email address was recently used to register the username '%s'
at <a href="https://quay.io">Quay</a>.<br>
at <a href="https://quay.io">Quay.io</a>.<br>
<br>
To confirm this email address, please click the following link:<br>
<a href="https://quay.io/confirm?code=%s">https://quay.io/confirm?code=%s</a>
"""
RECOVERY_MESSAGE = """
A user at <a href="https://quay.io">Quay.io</a> has attempted to recover their account
using this email.<br>
<br>
If you made this request, please click the following link to recover your account and
change your password:
<a href="https://quay.io/recovery?code=%s">https://quay.io/recovery?code=%s</a><br>
<br>
If you did not make this request, your account has not been compromised and the user was
not given access. Please disregard this email.<br>
"""
def send_confirmation_email(username, email, token):
msg = Message('Welcome to Quay! Please confirm your email.',
msg = Message('Welcome to Quay.io! Please confirm your email.',
sender='support@quay.io', # Why do I need this?
recipients=[email])
msg.html = CONFIRM_MESSAGE % (username, token, token)
mail.send(msg)
def send_recovery_email(email, token):
msg = Message('Quay.io account recovery.',
sender='support@quay.io', # Why do I need this?
recipients=[email])
msg.html = RECOVERY_MESSAGE % (token, token)
mail.send(msg)