Allowed users to change their own emails #45

Closed
Ghost wants to merge 2 commits from (deleted):main into main
12 changed files with 180 additions and 60 deletions

View file

@ -109,6 +109,27 @@ def magiclink(token):
abort(404, f"Token {token} doesn't exist or has already been used.") abort(404, f"Token {token} doesn't exist or has already been used.")
@bp.route("/email/<string:new_email>/<string:token>", 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") @bp.route("/logout")
def logout(): def logout():
session.clear() session.clear()

View file

@ -381,61 +381,98 @@ def get_payments():
@bp.route("/account-balance") @bp.route("/account", methods=("GET", "POST"))
@account_required @account_required
def account_balance(): def account():
payment_sessions = get_model().list_payment_sessions_for_account(session['account']) if request.method == "GET":
for payment_session in payment_sessions: payment_sessions = get_model().list_payment_sessions_for_account(session['account'])
if payment_session['type'] == 'btcpay': for payment_session in payment_sessions:
poll_btcpay_session(payment_session['id']) if payment_session['type'] == 'btcpay':
poll_btcpay_session(payment_session['id'])
payments = get_payments() payments = get_payments()
vms = get_vms() vms = get_vms()
balance_1w = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=7)) 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_1d = get_account_balance(vms, payments, datetime.utcnow() + timedelta(days=1))
balance_now = get_account_balance(vms, payments, datetime.utcnow()) balance_now = get_account_balance(vms, payments, datetime.utcnow())
warning_index = -1 warning_index = -1
warning_text = "" warning_text = ""
warnings = get_warnings_list() warnings = get_warnings_list()
for i in range(0, len(warnings)): for i in range(0, len(warnings)):
if warnings[i]['get_active'](balance_1w, balance_1d, balance_now): if warnings[i]['get_active'](balance_1w, balance_1d, balance_now):
warning_index = i warning_index = i
if warning_index > -1: if warning_index > -1:
pluralize_capsul = "s" if len(vms) > 1 else "" pluralize_capsul = "s" if len(vms) > 1 else ""
warning_id = warnings[warning_index]['id'] warning_id = warnings[warning_index]['id']
warning_text = get_warning_headline(warning_id, pluralize_capsul) 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')
))
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')}")
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')
))
return render_template( return render_template(
"account-balance.html", "account.html",
has_vms=len(vms_billed)>0, has_vms=len(vms_billed)>0,
vms_billed=vms_billed, vms_billed=vms_billed,
warning_text=warning_text, warning_text=warning_text,
btcpay_enabled=current_app.config["BTCPAY_URL"] != "", btcpay_enabled=current_app.config["BTCPAY_URL"] != "",
btcpay_is_working="BTCPAY_CLIENT" in current_app.config, btcpay_is_working="BTCPAY_CLIENT" in current_app.config,
payments=list(map( payments=list(map(
lambda x: dict( lambda x: dict(
dollars=x["dollars"], dollars=x["dollars"],
class_name="invalidated" if x["invalidated"] else "", class_name="invalidated" if x["invalidated"] else "",
created=x["created"].strftime("%b %d %Y") created=x["created"].strftime("%b %d %Y")
), ),
payments payments
)), )),
has_payments=len(payments)>0, has_payments=len(payments)>0,
account_balance=format(balance_now, '.2f') account_balance=format(balance_now, '.2f'),
) csrf_token=session["csrf-token"],
)

View file

@ -50,7 +50,7 @@ def init_app(app, is_running_server):
hasSchemaVersionTable = False hasSchemaVersionTable = False
actionWasTaken = False actionWasTaken = False
schemaVersion = 0 schemaVersion = 0
desiredSchemaVersion = 25 desiredSchemaVersion = 26
cursor = connection.cursor() cursor = connection.cursor()

View file

@ -512,3 +512,42 @@ class DBModel:
def set_broadcast_message(self, message): def set_broadcast_message(self, message):
self.cursor.execute("DELETE FROM broadcast_message; INSERT INTO broadcast_message (message) VALUES (%s)", (message, )) self.cursor.execute("DELETE FROM broadcast_message; INSERT INTO broadcast_message (message) VALUES (%s)", (message, ))
self.connection.commit() 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()

View file

@ -74,7 +74,7 @@ def btcpay_payment():
currency="USD", currency="USD",
itemDesc="Capsul Cloud Compute", itemDesc="Capsul Cloud Compute",
transactionSpeed="high", 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" notificationURL=f"{current_app.config['BASE_URL']}/payment/btcpay/webhook"
)) ))
except: except:

View file

@ -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;

View file

@ -104,7 +104,7 @@ def get_warnings_list():
get_subject=lambda _: "Capsul One Week Payment Reminder", get_subject=lambda _: "Capsul One Week Payment Reminder",
get_body=lambda base_url, pluralize_capsul: ( get_body=lambda base_url, pluralize_capsul: (
f"{get_warning_headline('zero_1w', 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" "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_subject=lambda _: "Capsul One Day Payment Reminder",
get_body=lambda base_url, pluralize_capsul: ( get_body=lambda base_url, pluralize_capsul: (
f"{get_warning_headline('zero_1d', 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" "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_subject=lambda _: "Your Capsul Account is No Longer Funded",
get_body=lambda base_url, pluralize_capsul: ( get_body=lambda base_url, pluralize_capsul: (
f"{get_warning_headline('zero_now', 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}, " 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" "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_subject=lambda pluralize_capsul: f"Your Capsul{pluralize_capsul} Will be Deleted In Less Than a Week",
get_body=lambda base_url, pluralize_capsul: ( get_body=lambda base_url, pluralize_capsul: (
f"{get_warning_headline('delete_1w', 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}, " 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" "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_subject=lambda pluralize_capsul: f"Last Chance to Save your Capsul{pluralize_capsul}: Gone Tomorrow",
get_body=lambda base_url, pluralize_capsul: ( get_body=lambda base_url, pluralize_capsul: (
f"{get_warning_headline('delete_1d', pluralize_capsul)}" f"{get_warning_headline('delete_1d', pluralize_capsul)}"
f"{base_url}/console/account-balance" f"{base_url}/console/account"
) )
), ),
dict( dict(

View file

@ -1,8 +1,9 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Account Balance{% endblock %} {% block title %}Account{% endblock %}
{% block content %} {% block content %}
<div class="row third-margin"> <div class="row third-margin">
<h1>Account Balance: ${{ account_balance }}</h1> <h1>Account Balance: ${{ account_balance }}</h1>
</div> </div>
@ -102,7 +103,17 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<h1>Current Account Email: {{ session["account"] }}</h1>
<form id="change-email" method="post" class="half-margin row">
<input type="hidden" name="method" value="POST"></input>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<label for="new-email">New Email Address</label>
<input name="new-email" type="text" required></input>
<input type="submit" value="Change">
</form>
<p>Once you click 'Change', you will receive an email at your new address.</p>
{% endblock %} {% endblock %}
{% block pagesource %}/templates/create-capsul.html{% endblock %} {% block pagesource %}/templates/create-capsul.html{% endblock %}

View file

@ -32,7 +32,7 @@
{% if session["account"] %} {% if session["account"] %}
<a href="/console/">Capsuls</a> <a href="/console/">Capsuls</a>
<a href="/console/ssh">SSH Public Keys</a> <a href="/console/ssh">SSH Public Keys</a>
<a href="/console/account-balance">Account Balance</a> <a href="/console/account">Account</a>
{% endif %} {% endif %}
<a href="/support">Support</a> <a href="/support">Support</a>

View file

@ -8,6 +8,7 @@
{% block subcontent %} {% block subcontent %}
<p> <p>
<ul> <ul>
<li>2023-01-20: Added support for users to change their own email addresses.</li>
<li>2022-07-18: Add NixOS support</li> <li>2022-07-18: Add NixOS support</li>
<li>2022-02-11: Added the <span class="code">/add-ssh-key-to-existing-capsul</span> page <li>2022-02-11: Added the <span class="code">/add-ssh-key-to-existing-capsul</span> page
<ul> <ul>

View file

@ -23,12 +23,12 @@
* all VMs come standard with one public IPv4 address * all VMs come standard with one public IPv4 address
* vms are billed for a minimum of 1 hour upon creation</pre> * vms are billed for a minimum of 1 hour upon creation</pre>
<pre> <pre>
Your <a href="/console/account-balance">current account balance</a>: ${{ account_balance }} {% if account_balance != account_balance_in_one_month %} Your <a href="/console/account">current account balance</a>: ${{ account_balance }} {% if account_balance != account_balance_in_one_month %}
Projected account balance in one month: ${{ account_balance_in_one_month }}{% endif %} Projected account balance in one month: ${{ account_balance_in_one_month }}{% endif %}
</pre> </pre>
{% if cant_afford %} {% if cant_afford %}
<p>Please <a href="/console/account-balance">fund your account</a> in order to create Capsuls</p> <p>Please <a href="/console/account">fund your account</a> in order to create Capsuls</p>
<p>(At least two hours worth of funding is required)</p> <p>(At least two hours worth of funding is required)</p>
{% elif no_ssh_public_keys %} {% elif no_ssh_public_keys %}
<p>You don't have any ssh public keys yet.</p> <p>You don't have any ssh public keys yet.</p>

View file

@ -69,6 +69,10 @@ $ doas su -</pre>
We promise to keep your machines running smoothly. We promise to keep your machines running smoothly.
If you lose access to your VM, that's on you.</p> If you lose access to your VM, that's on you.</p>
</li> </li>
<li id="backups">
<b><i>How do backups work?</i></b>
<p>Your VMs are backed up daily, and backups are kept for two days. To restore a backup, <a href="/support">contact support</a>.</p>
</li>
<li id="refunds"> <li id="refunds">
<b><i>Do you offer refunds?</i></b> <b><i>Do you offer refunds?</i></b>
<p>Not now, but email us and we can probably figure something out.</p> <p>Not now, but email us and we can probably figure something out.</p>