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)
user = ForeignKeyField(User)
pw_reset = BooleanField(default=False)
new_email = CharField(null=True, unique=True)
email_confirm = BooleanField(default=False)
created = DateTimeField(default=datetime.now)

View file

@ -337,8 +337,12 @@ def list_federated_logins(user):
FederatedLogin.user == user)
def create_confirm_email_code(user):
code = EmailConfirmation.create(user=user, email_confirm=True)
def create_confirm_email_code(user, new_email=None):
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
@ -347,15 +351,20 @@ def confirm_user_email(code):
code = EmailConfirmation.get(EmailConfirmation.code == code,
EmailConfirmation.email_confirm == True)
except EmailConfirmation.DoesNotExist:
raise DataModelException('Invalid email confirmation code.')
raise DataModelException('Invalid email confirmation code.')
user = code.user
user.verified = True
new_email = code.new_email
if new_email:
user.email = new_email
user.save()
code.delete_instance()
return user
return {'user': user, 'new_email': new_email}
def create_reset_password_email_code(email):
@ -384,6 +393,13 @@ def validate_reset_code(code):
return user
def find_user_by_email(email):
try:
return User.get(User.email == email)
except User.DoesNotExist:
return None
def get_user(username):
try:
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.plans import PLANS, get_plan
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.gravatar import compute_hash
@ -264,6 +264,20 @@ def change_user_details():
logger.debug('Changing invoice_email for user: %s', user.username)
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:
error_resp = jsonify({
'message': ex.message,

View file

@ -257,13 +257,13 @@ def confirm_email():
code = request.values['code']
try:
user = model.confirm_user_email(code)
result = model.confirm_user_email(code)
except model.DataModelException as ex:
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'])

View file

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

View file

@ -906,12 +906,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.loading = true;
$scope.updatingUser = false;
$scope.changePasswordSuccess = false;
$scope.changeEmailSent = false;
$scope.convertStep = 0;
$scope.org = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$('.form-change-pw').popover();
$('.form-change').popover();
$scope.logsShown = 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() {
$('.form-change-pw').popover('hide');
$('#changePasswordForm').popover('hide');
$scope.updatingUser = true;
$scope.changePasswordSuccess = false;
@ -991,7 +1015,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.changePasswordError = result.data.message;
$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="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="#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="#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>
@ -50,6 +51,36 @@
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></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 -->
<div id="password" class="tab-pane">
<div class="loading" ng-show="updatingUser">
@ -62,7 +93,7 @@
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<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">
<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"
@ -75,6 +106,7 @@
</div>
</div>
<!-- Github tab -->
<div id="github" class="tab-pane">
<div class="loading" ng-show="!cuser">
<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 = """
A user at <a href="https://quay.io">Quay.io</a> has attempted to recover their account
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):
msg = Message('Welcome to Quay.io! Please confirm your email.',
sender='support@quay.io', # Why do I need this?