From a363ada41c8d9fbbe6b9390aa7eb9de93f34feea Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 17 Jan 2014 17:04:05 -0500 Subject: [PATCH] =?UTF-8?q?Add=20ability=20to=20view=20and=20change=20an?= =?UTF-8?q?=20account=E2=80=99s=20email=20address?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/database.py | 1 + data/model.py | 24 +++++++++++++++++++---- endpoints/api.py | 16 +++++++++++++++- endpoints/web.py | 6 +++--- static/css/quay.css | 8 +++++++- static/js/controllers.js | 30 ++++++++++++++++++++++++++--- static/partials/user-admin.html | 34 ++++++++++++++++++++++++++++++++- util/email.py | 17 +++++++++++++++++ 8 files changed, 123 insertions(+), 13 deletions(-) diff --git a/data/database.py b/data/database.py index 49e1d607e..d32efb398 100644 --- a/data/database.py +++ b/data/database.py @@ -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) diff --git a/data/model.py b/data/model.py index 15eab7576..6859a4ac8 100644 --- a/data/model.py +++ b/data/model.py @@ -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) diff --git a/endpoints/api.py b/endpoints/api.py index 553ac77e1..ee82fd344 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -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, diff --git a/endpoints/web.py b/endpoints/web.py index d4a7d7651..cc914b169 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -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']) diff --git a/static/css/quay.css b/static/css/quay.css index dae3067e9..d4b09e778 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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; } diff --git a/static/js/controllers.js b/static/js/controllers.js index b9114a236..c37e68678 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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'); }); }); }; diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 324acfa37..275e0ee74 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -30,6 +30,7 @@
  • Billing Options
  • Billing History
  • Robot Accounts
  • +
  • Account E-mail
  • Change Password
  • GitHub Login
  • Usage Logs
  • @@ -50,6 +51,36 @@
    + +
    +
    +
    An e-mail has been sent to {{ sentEmail }} to verify the change.
    + +
    +
    +
    + +
    +
    Account e-mail address
    +
    + {{ user.email }} +
    +
    + +
    +
    Change e-mail address
    + +
    +
    + + +
    +
    +
    +
    +
    +
    @@ -62,7 +93,7 @@ Password changed successfully
    -
    +
    diff --git a/util/email.py b/util/email.py index 79cd2dd0f..6b838895e 100644 --- a/util/email.py +++ b/util/email.py @@ -12,6 +12,15 @@ To confirm this email address, please click the following link:
    """ +CHANGE_MESSAGE = """ +This email address was recently asked to become the new e-mail address for username '%s' +at Quay.io.
    +
    +To confirm this email address, please click the following link:
    +https://quay.io/confirm?code=%s +""" + + RECOVERY_MESSAGE = """ A user at Quay.io has attempted to recover their account using this email.
    @@ -25,6 +34,14 @@ not given access. Please disregard this email.
    """ +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?