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.