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:
parent
e182163d34
commit
4c15072c5a
17 changed files with 653 additions and 617 deletions
1
app.py
1
app.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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: '/'});
|
||||
}]).
|
||||
|
|
|
@ -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.
|
||||
|
|
53
static/partials/header.html
Normal file
53
static/partials/header.html
Normal 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 & 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 -->
|
72
static/partials/signin.html
Normal file
72
static/partials/signin.html
Normal 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> -->
|
|
@ -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 -->
|
||||
|
||||
{% 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>
|
|
@ -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 %}
|
|
@ -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 & 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 %}
|
|
@ -1,43 +1,10 @@
|
|||
<!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">
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_content %}
|
||||
<div class="container privacy-policy">
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
|
@ -115,5 +82,4 @@
|
|||
<a href="mailto:support@quay.io">support@quay.io</a>
|
||||
</dd>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,43 +1,10 @@
|
|||
<!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">
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
{% 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.io’s Privacy Policy) and procedures that may be published from time to time on this Site by DevTable (collectively, the “Agreement”).</p>
|
||||
|
@ -128,5 +95,4 @@
|
|||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
|
@ -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)
|
||||
|
|
Reference in a new issue