Add ability to view and change an account’s email address
This commit is contained in:
parent
d5bbea9fb2
commit
a363ada41c
8 changed files with 123 additions and 13 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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?
|
||||||
|
|
Reference in a new issue