From 69e2b1a024bf16af69b447d58115caa8a604f956 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Mar 2023 18:43:09 -0400 Subject: [PATCH] Added self-service email changes --- capsulflask/auth.py | 21 +++ capsulflask/console.py | 135 +++++++++++------- capsulflask/db.py | 2 +- capsulflask/db_model.py | 39 +++++ capsulflask/payment.py | 2 +- .../26_up_create_email_updates.sql | 7 + capsulflask/shared.py | 10 +- .../{account-balance.html => account.html} | 13 +- capsulflask/templates/base.html | 2 +- capsulflask/templates/changelog.html | 1 + capsulflask/templates/create-capsul.html | 4 +- capsulflask/templates/faq.html | 4 + 12 files changed, 180 insertions(+), 60 deletions(-) create mode 100644 capsulflask/schema_migrations/26_up_create_email_updates.sql rename capsulflask/templates/{account-balance.html => account.html} (83%) diff --git a/capsulflask/auth.py b/capsulflask/auth.py index f3731a2..6b3a44e 100644 --- a/capsulflask/auth.py +++ b/capsulflask/auth.py @@ -109,6 +109,27 @@ def magiclink(token): abort(404, f"Token {token} doesn't exist or has already been used.") +@bp.route("/email//", methods=("GET", )) +def email_change_confirmation(new_email, token): + email = get_model().check_email_change_token(token) + if email is not None: + if get_model().emails_contained_in_row(new_email, email) == True: + get_model().update_email(email, new_email) + + session.clear() + session["account"] = new_email + session["csrf-token"] = generate() + + return redirect(url_for("console.index")) + else: + abort(404, f"Email is invalid.") + else: + # this is here to prevent xss + if token and not re.match(r"^[a-zA-Z0-9_-]+$", token): + token = '___________' + + abort(404, f"Token {token} doesn't exist or has already been used.") + @bp.route("/logout") def logout(): session.clear() diff --git a/capsulflask/console.py b/capsulflask/console.py index 4b92b04..5dcf31b 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -381,61 +381,98 @@ def get_payments(): -@bp.route("/account-balance") +@bp.route("/account", methods=("GET", "POST")) @account_required -def account_balance(): +def account(): - payment_sessions = get_model().list_payment_sessions_for_account(session['account']) - for payment_session in payment_sessions: - if payment_session['type'] == 'btcpay': - poll_btcpay_session(payment_session['id']) + if request.method == "GET": + payment_sessions = get_model().list_payment_sessions_for_account(session['account']) + for payment_session in payment_sessions: + if payment_session['type'] == 'btcpay': + poll_btcpay_session(payment_session['id']) - payments = get_payments() - vms = get_vms() - balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7)) - balance_1d = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=1)) - balance_now = get_account_balance(vms, payments, datetime.utcnow()) + payments = get_payments() + vms = get_vms() + balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7)) + balance_1d = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=1)) + balance_now = get_account_balance(vms, payments, datetime.utcnow()) - warning_index = -1 - warning_text = "" - warnings = get_warnings_list() + warning_index = -1 + warning_text = "" + warnings = get_warnings_list() - for i in range(0, len(warnings)): - if warnings[i]['get_active'](balance_1w, balance_1d, balance_now): - warning_index = i - if warning_index > -1: - pluralize_capsul = "s" if len(vms) > 1 else "" - warning_id = warnings[warning_index]['id'] - warning_text = get_warning_headline(warning_id, pluralize_capsul) + for i in range(0, len(warnings)): + if warnings[i]['get_active'](balance_1w, balance_1d, balance_now): + warning_index = i + if warning_index > -1: + pluralize_capsul = "s" if len(vms) > 1 else "" + warning_id = warnings[warning_index]['id'] + warning_text = get_warning_headline(warning_id, pluralize_capsul) - vms_billed = list() + vms_billed = list() - for vm in get_vms(): - vm_months = get_vm_months_float(vm, datetime.utcnow()) - vms_billed.append(dict( - id=vm["id"], - dollars_per_month=vm["dollars_per_month"], - created=vm["created"].strftime("%b %d %Y"), - deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A", - months=format(vm_months, '.3f'), - dollars=format(vm_months * float(vm["dollars_per_month"]), '.2f') - )) + for vm in get_vms(): + vm_months = get_vm_months_float(vm, datetime.utcnow()) + vms_billed.append(dict( + id=vm["id"], + dollars_per_month=vm["dollars_per_month"], + created=vm["created"].strftime("%b %d %Y"), + deleted=vm["deleted"].strftime("%b %d %Y") if vm["deleted"] else "N/A", + months=format(vm_months, '.3f'), + dollars=format(vm_months * float(vm["dollars_per_month"]), '.2f') + )) + + if request.method == "POST": + if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: + return abort(418, f"u want tea") + + new_email = request.form["new-email"] + if new_email != session["account"]: + if re.fullmatch('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$', new_email): + token = generate() + + get_model().insert_email_update_row(new_email, session["account"], token) + + link = f"{current_app.config['BASE_URL']}/auth/email/{new_email}/{token}" + + message = (f"Navigate to {link} to finalize your email change.\n" + "\nIf you didn't request this, ignore this message.") + + if current_app.config["MAIL_SERVER"] != "": + current_app.config["FLASK_MAIL_INSTANCE"].send( + Message( + "Click This Link to change your Capsul email", + sender=current_app.config["MAIL_DEFAULT_SENDER"], + body=message, + recipients=[new_email] + ) + ) + else: + print("[DEBUG] Email change message: " + message) + + + + return redirect(f"{url_for('console.account')}") + + return render_template( - "account-balance.html", - has_vms=len(vms_billed)>0, - vms_billed=vms_billed, - warning_text=warning_text, - btcpay_enabled=current_app.config["BTCPAY_URL"] != "", - btcpay_is_working="BTCPAY_CLIENT" in current_app.config, - payments=list(map( - lambda x: dict( - dollars=x["dollars"], - class_name="invalidated" if x["invalidated"] else "", - created=x["created"].strftime("%b %d %Y") - ), - payments - )), - has_payments=len(payments)>0, - account_balance=format(balance_now, '.2f') - ) + "account.html", + has_vms=len(vms_billed)>0, + vms_billed=vms_billed, + warning_text=warning_text, + btcpay_enabled=current_app.config["BTCPAY_URL"] != "", + btcpay_is_working="BTCPAY_CLIENT" in current_app.config, + payments=list(map( + lambda x: dict( + dollars=x["dollars"], + class_name="invalidated" if x["invalidated"] else "", + created=x["created"].strftime("%b %d %Y") + ), + payments + )), + has_payments=len(payments)>0, + account_balance=format(balance_now, '.2f'), + csrf_token=session["csrf-token"], + ) + + diff --git a/capsulflask/db.py b/capsulflask/db.py index 72b3930..970d498 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -50,7 +50,7 @@ def init_app(app, is_running_server): hasSchemaVersionTable = False actionWasTaken = False schemaVersion = 0 - desiredSchemaVersion = 25 + desiredSchemaVersion = 26 cursor = connection.cursor() diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index bb68a29..6ca032a 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -512,3 +512,42 @@ class DBModel: def set_broadcast_message(self, message): self.cursor.execute("DELETE FROM broadcast_message; INSERT INTO broadcast_message (message) VALUES (%s)", (message, )) self.connection.commit() + + + def check_email_change_token(self, token): + self.cursor.execute("SELECT current_email FROM email_updates WHERE token = %s", (token, )) + row = self.cursor.fetchone() + if row: + email = row[0] + self.connection.commit() + return email + return None + + def emails_contained_in_row(self, new_email, old_email): + self.cursor.execute("SELECT current_email from email_updates WHERE current_email = %s AND new_email = %s", (old_email, new_email)) + row = self.cursor.fetchone() + if row: + self.connection.commit() + return True + self.connection.commit() + return False + + def insert_email_update_row(self, new_email, old_email, token): + self.cursor.execute("INSERT INTO email_updates (current_email, new_email, token) VALUES (%s, %s, %s)", (old_email, new_email, token)) + self.connection.commit() + pass + + def update_email(self, old_email, new_email): + self.cursor.execute("DELETE FROM email_updates WHERE current_email = %s", (old_email, )) + self.cursor.execute("UPDATE accounts SET lower_case_email = %s WHERE email = %s", (new_email.lower(), old_email.lower())) + self.cursor.execute("UPDATE accounts SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE login_tokens SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE operations SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE payment_sessions SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE payments SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE ssh_public_keys SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE unresolved_btcpay_invoices SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE vm_ssh_authorized_key SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE vm_ssh_host_key SET email = %s WHERE email = %s", (new_email, old_email)) + self.cursor.execute("UPDATE vms SET email = %s WHERE email = %s", (new_email, old_email)) + self.connection.commit() \ No newline at end of file diff --git a/capsulflask/payment.py b/capsulflask/payment.py index badf03a..5dd6559 100644 --- a/capsulflask/payment.py +++ b/capsulflask/payment.py @@ -74,7 +74,7 @@ def btcpay_payment(): currency="USD", itemDesc="Capsul Cloud Compute", transactionSpeed="high", - redirectURL=f"{current_app.config['BASE_URL']}/console/account-balance", + redirectURL=f"{current_app.config['BASE_URL']}/console/account", notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook" )) except: diff --git a/capsulflask/schema_migrations/26_up_create_email_updates.sql b/capsulflask/schema_migrations/26_up_create_email_updates.sql new file mode 100644 index 0000000..9963d7c --- /dev/null +++ b/capsulflask/schema_migrations/26_up_create_email_updates.sql @@ -0,0 +1,7 @@ +CREATE TABLE email_updates ( + current_email TEXT PRIMARY KEY NOT NULL, + new_email TEXT NOT NULL, + token TEXT NOT NULL +); + +UPDATE schemaversion SET version = 26; diff --git a/capsulflask/shared.py b/capsulflask/shared.py index b712204..2a9b0dd 100644 --- a/capsulflask/shared.py +++ b/capsulflask/shared.py @@ -104,7 +104,7 @@ def get_warnings_list(): get_subject=lambda _: "Capsul One Week Payment Reminder", get_body=lambda base_url, pluralize_capsul: ( f"{get_warning_headline('zero_1w', pluralize_capsul)}" - f"Log in now to re-fill your account! {base_url}/console/account-balance\n\n" + f"Log in now to re-fill your account! {base_url}/console/account\n\n" "If you believe you have recieved this message in error, please let us know: support@cyberia.club" ) ), @@ -114,7 +114,7 @@ def get_warnings_list(): get_subject=lambda _: "Capsul One Day Payment Reminder", get_body=lambda base_url, pluralize_capsul: ( f"{get_warning_headline('zero_1d', pluralize_capsul)}" - f"Log in now to re-fill your account! {base_url}/console/account-balance\n\n" + f"Log in now to re-fill your account! {base_url}/console/account\n\n" "If you believe you have recieved this message in error, please let us know: support@cyberia.club" ) ), @@ -124,7 +124,7 @@ def get_warnings_list(): get_subject=lambda _: "Your Capsul Account is No Longer Funded", get_body=lambda base_url, pluralize_capsul: ( f"{get_warning_headline('zero_now', pluralize_capsul)}" - f"Log in now to re-fill your account! {base_url}/console/account-balance\n\n" + f"Log in now to re-fill your account! {base_url}/console/account\n\n" f"If you need help decomissioning your Capsul{pluralize_capsul}, " "would like to request backups, or de-activate your account, please contact: support@cyberia.club" ) @@ -135,7 +135,7 @@ def get_warnings_list(): get_subject=lambda pluralize_capsul: f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week", get_body=lambda base_url, pluralize_capsul: ( f"{get_warning_headline('delete_1w', pluralize_capsul)}" - f"Log in now to re-fill your account! {base_url}/console/account-balance\n\n" + f"Log in now to re-fill your account! {base_url}/console/account\n\n" f"If you need help decomissioning your Capsul{pluralize_capsul}, " "would like to request backups, or de-activate your account, please contact: support@cyberia.club" ) @@ -146,7 +146,7 @@ def get_warnings_list(): get_subject=lambda pluralize_capsul: f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow", get_body=lambda base_url, pluralize_capsul: ( f"{get_warning_headline('delete_1d', pluralize_capsul)}" - f"{base_url}/console/account-balance" + f"{base_url}/console/account" ) ), dict( diff --git a/capsulflask/templates/account-balance.html b/capsulflask/templates/account.html similarity index 83% rename from capsulflask/templates/account-balance.html rename to capsulflask/templates/account.html index 75ce760..ddbad1a 100644 --- a/capsulflask/templates/account-balance.html +++ b/capsulflask/templates/account.html @@ -1,8 +1,9 @@ {% extends 'base.html' %} -{% block title %}Account Balance{% endblock %} +{% block title %}Account{% endblock %} {% block content %} +

Account Balance: ${{ account_balance }}

@@ -102,7 +103,17 @@ {% endif %} + +

Current Account Email: {{ session["account"] }}

+
+ + + + + +
+

Once you click 'Change', you will receive an email at your new address.

{% endblock %} {% block pagesource %}/templates/create-capsul.html{% endblock %} diff --git a/capsulflask/templates/base.html b/capsulflask/templates/base.html index 036b402..e36af6f 100644 --- a/capsulflask/templates/base.html +++ b/capsulflask/templates/base.html @@ -32,7 +32,7 @@ {% if session["account"] %} Capsuls SSH Public Keys - Account Balance + Account {% endif %} Support diff --git a/capsulflask/templates/changelog.html b/capsulflask/templates/changelog.html index ad6d246..ef63cbb 100644 --- a/capsulflask/templates/changelog.html +++ b/capsulflask/templates/changelog.html @@ -8,6 +8,7 @@ {% block subcontent %}

    +
  • 2023-01-20: Added support for users to change their own email addresses.
  • 2022-07-18: Add NixOS support
  • 2022-02-11: Added the /add-ssh-key-to-existing-capsul page
      diff --git a/capsulflask/templates/create-capsul.html b/capsulflask/templates/create-capsul.html index 57be57a..dd20fac 100644 --- a/capsulflask/templates/create-capsul.html +++ b/capsulflask/templates/create-capsul.html @@ -23,12 +23,12 @@ * all VMs come standard with one public IPv4 address * vms are billed for a minimum of 1 hour upon creation
      -    Your current account balance: ${{ account_balance }} {% if account_balance != account_balance_in_one_month  %} 
      +    Your current account balance: ${{ account_balance }} {% if account_balance != account_balance_in_one_month  %} 
       
           Projected account balance in one month: ${{ account_balance_in_one_month }}{% endif %}
         
      {% if cant_afford %} -

      Please fund your account in order to create Capsuls

      +

      Please fund your account in order to create Capsuls

      (At least two hours worth of funding is required)

      {% elif no_ssh_public_keys %}

      You don't have any ssh public keys yet.

      diff --git a/capsulflask/templates/faq.html b/capsulflask/templates/faq.html index efe8ffa..eae5b91 100644 --- a/capsulflask/templates/faq.html +++ b/capsulflask/templates/faq.html @@ -69,6 +69,10 @@ $ doas su - We promise to keep your machines running smoothly. If you lose access to your VM, that's on you.

      +
    • + How do backups work? +

      Your VMs are backed up daily, and backups are kept for two days. To restore a backup, contact support.

      +
    • Do you offer refunds?

      Not now, but email us and we can probably figure something out.

      -- 2.40.1