self-service-change-email #47
16 changed files with 195 additions and 24 deletions
|
@ -15,7 +15,7 @@ Python Flask web application implementing user accounts, payment, and virtual ma
|
||||||
```
|
```
|
||||||
# get an instance of postgres running locally on port 5432
|
# get an instance of postgres running locally on port 5432
|
||||||
# (you don't have to use docker, but we thought this might be the easiest for a how-to example)
|
# (you don't have to use docker, but we thought this might be the easiest for a how-to example)
|
||||||
docker run --rm -it -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres &
|
docker run --rm -it -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
sudo apt install pipenv python3-dev libpq-dev
|
sudo apt install pipenv python3-dev libpq-dev
|
||||||
|
|
|
@ -16,6 +16,7 @@ from flask_mail import Message
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from capsulflask.db import get_model
|
from capsulflask.db import get_model
|
||||||
|
from capsulflask.shared import validate_email
|
||||||
|
|
||||||
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||||
|
|
||||||
|
@ -53,10 +54,9 @@ def login():
|
||||||
email = request.form["email"]
|
email = request.form["email"]
|
||||||
errors = list()
|
errors = list()
|
||||||
|
|
||||||
if not email:
|
email_error = validate_email(email)
|
||||||
errors.append("email is required")
|
if email_error != None:
|
||||||
elif len(email.strip()) < 6 or email.count('@') != 1 or email.count('.') == 0:
|
errors.append(email_error)
|
||||||
errors.append("enter a valid email address")
|
|
||||||
|
|
||||||
if len(errors) == 0:
|
if len(errors) == 0:
|
||||||
result = get_model().login(email)
|
result = get_model().login(email)
|
||||||
|
@ -109,6 +109,24 @@ 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("/change-email/<string:token>", methods=("GET", ))
|
||||||
|
def email_change_landing(token):
|
||||||
|
return render_template("change-email-landing.html", token=token)
|
||||||
|
|
||||||
|
@bp.route("/change-email/<string:token>", methods=("POST",))
|
||||||
|
@account_required
|
||||||
|
def email_change_confirmation(token):
|
||||||
|
new_email, status_code, error_message = get_model().try_update_email(session["account"], token)
|
||||||
|
if error_message:
|
||||||
|
abort(status_code, error_message)
|
||||||
|
return
|
||||||
|
|
||||||
|
session.clear()
|
||||||
|
session["account"] = new_email
|
||||||
|
session["csrf-token"] = generate()
|
||||||
|
|
||||||
|
return redirect(url_for("console.index"))
|
||||||
|
|
||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
session.clear()
|
session.clear()
|
||||||
|
|
|
@ -17,7 +17,7 @@ from nanoid import generate
|
||||||
from capsulflask.metrics import metric_durations
|
from capsulflask.metrics import metric_durations
|
||||||
from capsulflask.auth import account_required
|
from capsulflask.auth import account_required
|
||||||
from capsulflask.db import get_model
|
from capsulflask.db import get_model
|
||||||
from capsulflask.shared import my_exec_info_message, get_warnings_list, get_warning_headline, get_vm_months_float, get_account_balance, average_number_of_days_in_a_month
|
from capsulflask.shared import my_exec_info_message, get_warnings_list, get_warning_headline, get_vm_months_float, get_account_balance, validate_email, average_number_of_days_in_a_month
|
||||||
from capsulflask.payment import poll_btcpay_session, check_if_shortterm_flag_can_be_unset
|
from capsulflask.payment import poll_btcpay_session, check_if_shortterm_flag_can_be_unset
|
||||||
from capsulflask import cli
|
from capsulflask import cli
|
||||||
|
|
||||||
|
@ -381,10 +381,9 @@ def get_payments():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/account-balance")
|
@bp.route("/account", methods=("GET",))
|
||||||
@account_required
|
@account_required
|
||||||
def account_balance():
|
def account():
|
||||||
|
|
||||||
payment_sessions = get_model().list_payment_sessions_for_account(session['account'])
|
payment_sessions = get_model().list_payment_sessions_for_account(session['account'])
|
||||||
for payment_session in payment_sessions:
|
for payment_session in payment_sessions:
|
||||||
if payment_session['type'] == 'btcpay':
|
if payment_session['type'] == 'btcpay':
|
||||||
|
@ -422,7 +421,7 @@ def account_balance():
|
||||||
))
|
))
|
||||||
|
|
||||||
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,
|
||||||
|
@ -437,5 +436,52 @@ def account_balance():
|
||||||
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"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/account", methods=("POST",))
|
||||||
|
@account_required
|
||||||
|
def change_email_address():
|
||||||
|
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"]
|
||||||
|
|
||||||
|
email_error = validate_email(new_email)
|
||||||
|
if email_error != None:
|
||||||
|
flash(email_error)
|
||||||
|
return redirect(f"{url_for('console.account')}")
|
||||||
|
|
||||||
|
token = generate()
|
||||||
|
|
||||||
|
email_update_error = get_model().try_insert_email_update_row(session['account'], new_email, token)
|
||||||
|
if email_update_error != None:
|
||||||
|
flash(email_update_error)
|
||||||
|
return redirect(f"{url_for('console.account')}")
|
||||||
|
|
||||||
|
link = f"{current_app.config['BASE_URL']}/auth/change-email/{token}"
|
||||||
|
|
||||||
|
message = (f"Navigate to {link} to finalize your email change.\n" + "\nIf you didn't request this, you may 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]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
current_app.logger.info(f"Email change {session['account']} -> {new_email} email was sent.")
|
||||||
|
|
||||||
|
flash(f"An email has been sent to {new_email}. Click the link in that email to finalize your email change.")
|
||||||
|
else:
|
||||||
|
current_app.logger.info(f"Email change [{session['account']} -> {new_email}] message: {message}")
|
||||||
|
|
||||||
|
flash(f"No email server is configured. The email message was printed to the log.")
|
||||||
|
|
||||||
|
return redirect(f"{url_for('console.account')}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -512,3 +512,52 @@ 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 try_insert_email_update_row(self, old_email, new_email, token):
|
||||||
|
|
||||||
|
self.cursor.execute("SELECT new_email from email_updates WHERE new_email = %s OR new_email = %s", (new_email, new_email.lower()))
|
||||||
|
already_existing_email_update = self.cursor.fetchone()
|
||||||
|
self.cursor.execute("SELECT email from accounts WHERE email = %s or lower_case_email = %s", (new_email, new_email.lower()))
|
||||||
|
already_existing_account = self.cursor.fetchone()
|
||||||
|
|
||||||
|
if already_existing_email_update or already_existing_account:
|
||||||
|
return "another account is already using that email address"
|
||||||
|
|
||||||
|
self.cursor.execute("INSERT INTO email_updates (current_email, new_email, token) VALUES (%s, %s, %s)", (old_email, new_email, token))
|
||||||
|
self.connection.commit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def try_update_email(self, old_email, token):
|
||||||
|
|
||||||
|
self.cursor.execute("SELECT new_email from email_updates WHERE current_email = %s AND token = %s", (old_email, token))
|
||||||
|
row = self.cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
# this is here to prevent xss
|
||||||
|
if token and not re.match(r"^[a-zA-Z0-9_-]+$", token):
|
||||||
|
token = '___________'
|
||||||
|
|
||||||
|
return None, 404, f"Token '{token}' doesn't exist or has already been used."
|
||||||
|
|
||||||
|
new_email = row[0]
|
||||||
|
self.cursor.execute("SELECT email from accounts WHERE email = %s or lower_case_email = %s", (new_email, new_email.lower()))
|
||||||
|
already_existing_account = self.cursor.fetchone()
|
||||||
|
|
||||||
|
if already_existing_account:
|
||||||
|
return None, 400, "another account is already using that email address."
|
||||||
|
|
||||||
|
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))
|
||||||
|
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()
|
||||||
|
|
||||||
|
return new_email, None, None
|
|
@ -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:
|
||||||
|
|
3
capsulflask/schema_migrations/26_down_email_updates.sql
Normal file
3
capsulflask/schema_migrations/26_down_email_updates.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
DROP TABLE email_updates;
|
||||||
|
|
||||||
|
UPDATE schemaversion SET version = 25;
|
7
capsulflask/schema_migrations/26_up_email_updates.sql
Normal file
7
capsulflask/schema_migrations/26_up_email_updates.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE email_updates (
|
||||||
|
current_email TEXT NOT NULL,
|
||||||
|
new_email TEXT PRIMARY KEY NOT NULL,
|
||||||
|
token TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE schemaversion SET version = 26;
|
|
@ -60,6 +60,14 @@ def authorized_as_hub(headers):
|
||||||
return auth_header_value == current_app.config["HUB_TOKEN"]
|
return auth_header_value == current_app.config["HUB_TOKEN"]
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def validate_email(email):
|
||||||
|
if not email:
|
||||||
|
return "email is required"
|
||||||
|
elif len(email.strip()) < 6 or email.count('@') != 1 or email.split('@')[1].count('.') == 0:
|
||||||
|
return "enter a valid email address"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_account_balance(vms, payments, as_of):
|
def get_account_balance(vms, payments, as_of):
|
||||||
|
|
||||||
vm_cost_dollars = 0.0
|
vm_cost_dollars = 0.0
|
||||||
|
@ -104,7 +112,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 +122,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 +132,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 +143,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 +154,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(
|
||||||
|
|
7
capsulflask/static/change-email-landing.js
Normal file
7
capsulflask/static/change-email-landing.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function(){
|
||||||
|
document.querySelector("form").submit()
|
||||||
|
document.querySelector("form input").style.display = "none";
|
||||||
|
const pleaseWaitMessage = document.createElement("p");
|
||||||
|
pleaseWaitMessage.textContent = "Please wait while your email change is finalized..."
|
||||||
|
document.querySelector("form").appendChild(pleaseWaitMessage);
|
||||||
|
})
|
|
@ -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>
|
||||||
|
@ -44,7 +45,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a>
|
<a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a>
|
||||||
<ul><li>notice: stripe will load nonfree javascript </li></ul>
|
<ul><li>notice: will load stripe's javascript</li></ul>
|
||||||
</li>
|
</li>
|
||||||
{% if btcpay_enabled %}
|
{% if btcpay_enabled %}
|
||||||
{% if btcpay_is_working %}
|
{% if btcpay_is_working %}
|
||||||
|
@ -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 id="new-email" 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 %}
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!-- Namecoin Address: N2aVL6pHtBp7EtNGb3jpsL2L2NyjBNbiB1 -->
|
<!-- Namecoin Address: N2aVL6pHtBp7EtNGb3jpsL2L2NyjBNbiB1 -->
|
||||||
|
@ -32,7 +33,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>
|
||||||
|
|
16
capsulflask/templates/change-email-landing.html
Normal file
16
capsulflask/templates/change-email-landing.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="/static/change-email-landing.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}changing email...{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="/auth/change-email/{{token}}">
|
||||||
|
<input type="submit" value="Click here to finalize email change"/>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagesource %}/templates/change-email-landing.html{% endblock %}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>Capsuls are backed up daily, and backups are kept for two days. To restore a backup, contact support: <a href="mailto:support@cyberia.club?subject=restore%20backup">support@cyberia.club</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>
|
||||||
|
|
Loading…
Reference in a new issue