Add ability to view and change an account’s email address

This commit is contained in:
Joseph Schorr 2014-01-17 17:04:05 -05:00
parent d5bbea9fb2
commit a363ada41c
8 changed files with 123 additions and 13 deletions

View file

@ -154,6 +154,7 @@ class EmailConfirmation(BaseModel):
code = CharField(default=random_string_generator(), unique=True, index=True) code = CharField(default=random_string_generator(), unique=True, index=True)
user = ForeignKeyField(User) user = ForeignKeyField(User)
pw_reset = BooleanField(default=False) pw_reset = BooleanField(default=False)
new_email = CharField(null=True, unique=True)
email_confirm = BooleanField(default=False) email_confirm = BooleanField(default=False)
created = DateTimeField(default=datetime.now) created = DateTimeField(default=datetime.now)

View file

@ -337,8 +337,12 @@ def list_federated_logins(user):
FederatedLogin.user == user) FederatedLogin.user == user)
def create_confirm_email_code(user): def create_confirm_email_code(user, new_email=None):
code = EmailConfirmation.create(user=user, email_confirm=True) if new_email:
if not validate_email(new_email):
raise InvalidEmailAddressException('Invalid email address: %s' % new_email)
code = EmailConfirmation.create(user=user, email_confirm=True, new_email=new_email)
return code return code
@ -347,15 +351,20 @@ def confirm_user_email(code):
code = EmailConfirmation.get(EmailConfirmation.code == code, code = EmailConfirmation.get(EmailConfirmation.code == code,
EmailConfirmation.email_confirm == True) EmailConfirmation.email_confirm == True)
except EmailConfirmation.DoesNotExist: except EmailConfirmation.DoesNotExist:
raise DataModelException('Invalid email confirmation code.') raise DataModelException('Invalid email confirmation code.')
user = code.user user = code.user
user.verified = True user.verified = True
new_email = code.new_email
if new_email:
user.email = new_email
user.save() user.save()
code.delete_instance() code.delete_instance()
return user return {'user': user, 'new_email': new_email}
def create_reset_password_email_code(email): def create_reset_password_email_code(email):
@ -384,6 +393,13 @@ def validate_reset_code(code):
return user return user
def find_user_by_email(email):
try:
return User.get(User.email == email)
except User.DoesNotExist:
return None
def get_user(username): def get_user(username):
try: try:
return User.get(User.username == username, User.organization == False) return User.get(User.username == username, User.organization == False)

View file

@ -14,7 +14,7 @@ from data import model
from data.queue import dockerfile_build_queue from data.queue import dockerfile_build_queue
from data.plans import PLANS, get_plan from data.plans import PLANS, get_plan
from app import app from app import app
from util.email import send_confirmation_email, send_recovery_email from util.email import send_confirmation_email, send_recovery_email, send_change_email
from util.names import parse_repository_name, format_robot_username from util.names import parse_repository_name, format_robot_username
from util.gravatar import compute_hash from util.gravatar import compute_hash
@ -264,6 +264,20 @@ def change_user_details():
logger.debug('Changing invoice_email for user: %s', user.username) logger.debug('Changing invoice_email for user: %s', user.username)
model.change_invoice_email(user, user_data['invoice_email']) model.change_invoice_email(user, user_data['invoice_email'])
if 'email' in user_data and user_data['email'] != user.email:
new_email = user_data['email']
if model.find_user_by_email(new_email):
# Email already used.
error_resp = jsonify({
'message': 'E-mail address already used'
})
error_resp.status_code = 400
return error_resp
logger.debug('Sending email to change email address for user: %s', user.username)
code = model.create_confirm_email_code(user, new_email=new_email)
send_change_email(user.username, user_data['email'], code.code)
except model.InvalidPasswordException, ex: except model.InvalidPasswordException, ex:
error_resp = jsonify({ error_resp = jsonify({
'message': ex.message, 'message': ex.message,

View file

@ -257,13 +257,13 @@ def confirm_email():
code = request.values['code'] code = request.values['code']
try: try:
user = model.confirm_user_email(code) result = model.confirm_user_email(code)
except model.DataModelException as ex: except model.DataModelException as ex:
return redirect(url_for('signin')) return redirect(url_for('signin'))
common_login(user) common_login(result['user'])
return redirect(url_for('index')) return redirect(url_for('user', tab='email') if result['new_email'] else url_for('index'))
@app.route('/recovery', methods=['GET']) @app.route('/recovery', methods=['GET'])

View file

@ -1850,6 +1850,12 @@ p.editable:hover i {
text-align: center; text-align: center;
} }
.user-admin .panel-setting-content {
margin-left: 16px;
margin-top: 16px;
font-family: monospace;
}
.user-admin .plan-description { .user-admin .plan-description {
font-size: 1.2em; font-size: 1.2em;
margin-bottom: 10px; margin-bottom: 10px;
@ -1860,7 +1866,7 @@ p.editable:hover i {
margin-bottom: 10px; margin-bottom: 10px;
} }
.user-admin .form-change-pw input { .user-admin .form-change input {
margin-top: 12px; margin-top: 12px;
margin-bottom: 12px; margin-bottom: 12px;
} }

View file

@ -906,12 +906,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.loading = true; $scope.loading = true;
$scope.updatingUser = false; $scope.updatingUser = false;
$scope.changePasswordSuccess = false; $scope.changePasswordSuccess = false;
$scope.changeEmailSent = false;
$scope.convertStep = 0; $scope.convertStep = 0;
$scope.org = {}; $scope.org = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri; $scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId; $scope.githubClientId = KeyService.githubClientId;
$('.form-change-pw').popover(); $('.form-change').popover();
$scope.logsShown = 0; $scope.logsShown = 0;
$scope.invoicesShown = 0; $scope.invoicesShown = 0;
@ -970,8 +971,31 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
}); });
}; };
$scope.changeEmail = function() {
$('#changeEmailForm').popover('hide');
$scope.updatingUser = true;
$scope.changeEmailSent = false;
ApiService.changeUserDetails($scope.cuser).then(function() {
$scope.updatingUser = false;
$scope.changeEmailSent = true;
$scope.sentEmail = $scope.cuser.email;
// Reset the form.
$scope.cuser.repeatEmail = '';
$scope.changeEmailForm.$setPristine();
}, function(result) {
$scope.updatingUser = false;
$scope.changeEmailError = result.data.message;
$timeout(function() {
$('#changeEmailForm').popover('show');
});
});
};
$scope.changePassword = function() { $scope.changePassword = function() {
$('.form-change-pw').popover('hide'); $('#changePasswordForm').popover('hide');
$scope.updatingUser = true; $scope.updatingUser = true;
$scope.changePasswordSuccess = false; $scope.changePasswordSuccess = false;
@ -991,7 +1015,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.changePasswordError = result.data.message; $scope.changePasswordError = result.data.message;
$timeout(function() { $timeout(function() {
$('.form-change-pw').popover('show'); $('#changePasswordForm').popover('show');
}); });
}); });
}; };

View file

@ -30,6 +30,7 @@
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li> <li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li> <li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li> <li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li>
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li> <li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
@ -50,6 +51,36 @@
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div> <div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
</div> </div>
<!-- E-mail address tab -->
<div id="email" class="tab-pane">
<div class="row">
<div class="alert alert-success" ng-show="changeEmailSent">An e-mail has been sent to {{ sentEmail }} to verify the change.</div>
<div class="loading" ng-show="updatingUser">
<div class="quay-spinner 3x"></div>
</div>
<div class="panel" ng-show="!updatingUser">
<div class="panel-title">Account e-mail address</div>
<div class="panel-setting-content">
{{ user.email }}
</div>
</div>
<div class="panel" ng-show="!updatingUser" >
<div class="panel-title">Change e-mail address</div>
<div class="panel-body">
<form class="form-change col-md-6" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
data-content="{{ changeEmailError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="email" class="form-control" placeholder="Your new e-mail address" ng-model="cuser.email" required>
<button class="btn btn-primary" ng-disabled="changeEmailForm.$invalid || cuser.email == user.email" type="submit">Change E-mail Address</button>
</form>
</div>
</div>
</div>
</div>
<!-- Change password tab --> <!-- Change password tab -->
<div id="password" class="tab-pane"> <div id="password" class="tab-pane">
<div class="loading" ng-show="updatingUser"> <div class="loading" ng-show="updatingUser">
@ -62,7 +93,7 @@
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span> <span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<div ng-show="!updatingUser" class="panel-body"> <div ng-show="!updatingUser" class="panel-body">
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual" <form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering"> data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required> <input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword" <input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
@ -75,6 +106,7 @@
</div> </div>
</div> </div>
<!-- Github tab -->
<div id="github" class="tab-pane"> <div id="github" class="tab-pane">
<div class="loading" ng-show="!cuser"> <div class="loading" ng-show="!cuser">
<div class="quay-spinner 3x"></div> <div class="quay-spinner 3x"></div>

View file

@ -12,6 +12,15 @@ To confirm this email address, please click the following link:<br>
""" """
CHANGE_MESSAGE = """
This email address was recently asked to become the new e-mail address for username '%s'
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 = """ RECOVERY_MESSAGE = """
A user at <a href="https://quay.io">Quay.io</a> has attempted to recover their account A user at <a href="https://quay.io">Quay.io</a> has attempted to recover their account
using this email.<br> using this email.<br>
@ -25,6 +34,14 @@ not given access. Please disregard this email.<br>
""" """
def send_change_email(username, email, token):
msg = Message('Quay.io email change. Please confirm your email.',
sender='support@quay.io', # Why do I need this?
recipients=[email])
msg.html = CHANGE_MESSAGE % (username, token, token)
mail.send(msg)
def send_confirmation_email(username, email, token): def send_confirmation_email(username, email, token):
msg = Message('Welcome to Quay.io! Please confirm your email.', msg = Message('Welcome to Quay.io! Please confirm your email.',
sender='support@quay.io', # Why do I need this? sender='support@quay.io', # Why do I need this?