improve account pages

redesign the account overview and settings pages;
remove the logs page;
add account suppression;
add data export (with logs, wg keys, ...)
master
Alice 4 years ago
parent 7fc4204960
commit a79fe855aa

@ -198,6 +198,10 @@ RECAPTCHA_API = 'https://www.google.com/recaptcha/api/siteverify'
RECAPTCHA_SITE_KEY = ''
RECAPTCHA_SECRET_KEY = ''
HCAPTCHA_API = 'https://hcaptcha.com/siteverify'
HCAPTCHA_SITE_KEY = ''
HCAPTCHA_SECRET_KEY = ''
# lcore API settings
LCORE = dict(
BASE_URL='https://core.test.lambdavpn.net/v1/',

@ -41,6 +41,15 @@ class VPNUser(models.Model, LcoreUserProfileMethods):
referrer_used = models.BooleanField(default=False)
campaign = models.CharField(blank=True, null=True, max_length=64)
def clear_fields(self):
self.expiration = None
self.last_expiry_notice = None
self.last_vpn_auth = None
self.last_core_sync = None
self.referrer = None
self.referrer_used = False
self.campaign = None
def give_trial_period(self):
self.add_paid_time(get_trial_period_duration())
self.trial_periods_given += 1

@ -12,12 +12,11 @@ urlpatterns = [
path('logout', views.logout, name='logout'),
path('signup', views.signup, name='signup'),
path('settings', views.settings),
path('settings', views.settings, name='account_settings'),
path('config', views.config),
path('config_dl', django_lcore.views.openvpn_dl),
path('wireguard', views.wireguard),
path('wireguard/new', views.wireguard_new, name='wireguard_new'),
path('logs', views.logs),
path('gift_code', views.gift_code),
path('trial', views.trial),
path('', views.index, name='index'),

@ -12,6 +12,7 @@ from django.contrib.admin.sites import site
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.db.models import Count
from django.db import transaction
from django.http import (HttpResponse,
HttpResponseNotFound, HttpResponseRedirect,
JsonResponse)
@ -177,7 +178,7 @@ def index(request):
if b.backend_has_recurring),
key=lambda x: x.backend_id),
default_backend='paypal',
recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY,
hcaptcha_site_key=project_settings.HCAPTCHA_SITE_KEY,
price=price_fn(),
user_motd=site_config.MOTD_USER,
)
@ -185,14 +186,13 @@ def index(request):
def captcha_test(grr, request):
api_url = project_settings.RECAPTCHA_API
api_url = project_settings.HCAPTCHA_API
if api_url == 'TEST' and grr == 'TEST-TOKEN':
# FIXME: i'm sorry.
return True
data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY,
remoteip=get_client_ip(request),
data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY,
response=grr)
try:
@ -220,30 +220,170 @@ def trial(request):
return redirect('account:index')
def make_export_zip(user, name):
import io
import zipfile
import json
f = io.BytesIO()
z = zipfile.ZipFile(f, mode="w")
gw_cache = {}
def process_wg_peer(item):
keys = {"gateway_port", "id", "local_ipv4", "local_ipv6", "name", "object",
"private_key", "public_key"}
return {k: v for (k, v) in item.items() if k in keys}
def process_ovpn_sess(item):
keys = {"connect_date", "disconnect_date", "remote", "object", "protocol", "id",
"stats", "tunnel"}
def convert(v):
if isinstance(v, datetime):
return v.isoformat()
return v
obj = {k: convert(v) for (k, v) in item.items() if k in keys}
gw_url = item["gateway"]["href"]
if gw_url not in gw_cache:
gw = django_lcore.api.get(gw_url)
gw_cache[gw_url] = {
"name": gw["name"],
"ipv4": gw["main_ipv4"],
"ipv6": gw["main_ipv6"],
}
obj["gateway"] = gw_cache[gw_url]
return obj
def process_payments(item):
obj = {
"id": item.id,
"backend": item.backend_id,
"status": item.status,
"created": item.created.isoformat(),
"confirmed": item.confirmed_on.isoformat() if item.confirmed_on else None,
"amount": item.amount / 100,
"paid_amount": item.amount / 100,
"time": item.time.total_seconds(),
"external_id": item.backend_extid,
}
if item.subscription:
obj["subscription"] = item.subscription.backend_extid
else:
obj["subscription"] = None
return obj
with z.open(name + "/account.json", "w") as jf:
jf.write(json.dumps({
"username": user.username,
"email": user.email,
"date_joined": user.date_joined.isoformat(),
"expiration": user.vpnuser.expiration.isoformat() if user.vpnuser.expiration else None,
}, indent=2).encode('ascii'))
with z.open(name + "/wireguard_peers.json", "w") as jf:
try:
keys = map(process_wg_peer, django_lcore.api.get_wg_peers(user.username))
except lcoreapi.APINotFoundError:
keys = []
jf.write(json.dumps(keys, indent=2).encode('ascii'))
with z.open(name + "/openvpn_logs.json", "w") as jf:
base = django_lcore.api.info['current_instance']
next_page = '/users/' + user.username + '/sessions/'
try:
items = django_lcore.api.get(base + next_page).list_iter()
except lcoreapi.APINotFoundError:
items = []
items = list(map(process_ovpn_sess, items))
jf.write(json.dumps(items, indent=2).encode('ascii'))
with z.open(name + "/payments.json", "w") as jf:
items = user.payment_set.all()
items = list(map(process_payments, items))
jf.write(json.dumps(items, indent=2).encode('ascii'))
z.close()
return f.getvalue()
def deactivate_user(user):
""" clear most information from a user, keeping the username and id """
user.vpnuser.clear_fields()
user.vpnuser.save()
user.is_active = False
user.email = ""
user.password = ""
user.save()
user.giftcodeuser_set.all().delete()
user.payment_set.update(backend_data="null")
user.subscription_set.update(backend_data="null")
django_lcore.sync_user(user.vpnuser)
@login_required
def settings(request):
can_delete = request.user.vpnuser.get_subscription() is None
if request.method != 'POST':
return render(request, 'lambdainst/settings.html')
return render(request, 'lambdainst/settings.html', dict(
can_delete=can_delete,
))
pw = request.POST.get('password')
pw2 = request.POST.get('password2')
if pw and pw2:
if pw != pw2:
messages.error(request, _("Passwords do not match"))
else:
request.user.set_password(pw)
django_lcore.sync_user(request.user.vpnuser)
messages.success(request, _("OK!"))
action = request.POST.get('action')
current_pw = request.POST.get('current_password')
if not request.user.check_password(current_pw):
messages.error(request, _("Invalid password"))
return redirect('lambdainst:account_settings')
if action == 'email':
email = request.POST.get('email')
if email:
request.user.email = email
messages.success(request, _("OK! Email address changed."))
else:
request.user.email = ''
messages.success(request, _("OK! Email address unset."))
request.user.save()
return redirect('lambdainst:account_settings')
return render(request, 'lambdainst/settings.html', dict(title=_("Settings")))
elif action == 'password':
pw = request.POST.get('password')
pw2 = request.POST.get('password2')
if pw != pw2 or not pw:
messages.error(request, _("Password and confirmation do not match"))
else:
request.user.set_password(pw)
django_lcore.sync_user(request.user.vpnuser)
messages.success(request, _("OK! Password changed."))
request.user.save()
return redirect('lambdainst:account_settings')
elif action == 'export':
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
data = make_export_zip(request.user, timestamp)
r = HttpResponse(content=data, content_type="application/zip")
r["Content-Disposition"] = 'attachment; filename="ccvpn-export-{}-{}.zip"'.format(request.user.username, timestamp)
return r
elif action == 'delete' and can_delete:
with transaction.atomic():
deactivate_user(request.user)
logout(request)
messages.success(request, _("OK! Your account has been deactivated."))
return redirect('/')
return render(request, 'lambdainst/settings.html', dict(
title=_("Settings"),
user=request.user,
can_delete=can_delete,
))
@login_required
@ -263,31 +403,6 @@ def gift_code(request):
return redirect('account:index')
@login_required
def logs(request):
page_size = 20
page = int(request.GET.get('page', 0))
offset = page * page_size
base = django_lcore.api.info['current_instance']
path = '/users/' + request.user.username + '/sessions/'
try:
l = django_lcore.api.get(base + path, offset=offset, limit=page_size)
total_count = l['total_count']
items = l['items']
except lcoreapi.APINotFoundError:
total_count = 0
items = []
return render(request, 'lambdainst/logs.html', {
'sessions': items,
'page': page,
'prev': page - 1 if page > 0 else None,
'next': page + 1 if offset + page_size < total_count else None,
'last_page': total_count // page_size,
'title': _("Logs"),
})
@login_required
def config(request):
return render(request, 'lambdainst/config.html', dict(

@ -36,6 +36,9 @@ pre {
div#captcha > div {
margin: 1em auto;
}
.h-captcha {
text-align: center;
}
/* Firefox fix */
.formpage.pure-g {
@ -218,6 +221,7 @@ header nav a{
.flex-page {
display: flex;
margin: 0 auto 0 auto;
}
.flex-page .left-menu {
display: block;
@ -228,6 +232,16 @@ header nav a{
flex-shrink: 0;
}
.flex-page-content {
overflow: auto;
flex-grow: 2;
margin: 2em 1em 2em 1em;
}
.flex-page-content .content-box {
margin: 0;
margin-top: 1em;
}
.content-box {
margin: 1em;
border: 1px solid #bbb;
@ -241,7 +255,7 @@ header nav a{
box-shadow: 1px 1px 2px 1px rgba(0,0,0,0.21);
}
.content-box h2 {
.flex-page-content h2, .content-box h2 {
margin-top: 0;
font-size: 1.5em;
font-weight: bold;
@ -279,8 +293,6 @@ box-shadow: 1px 1px 2px 1px rgba(0,0,0,0.21);
.kb-question-list a {
font-size: 1.2em;
}*/
.kb-question-meta {
}
.kb-question-meta .kb-vote-buttons {
padding-left: 0.5em;
}
@ -342,6 +354,18 @@ table.admin-list tbody tr:last-child td { border-bottom: 0; }
table.admin-list tbody tr:nth-child(even) { background: #eee; }
/***************************************************/
/********************* Account Pages */
.account-settings > div {
margin: 0 auto 2em auto;
}
.account-settings .content-box {
padding: 0em 1em;
}
/***************************************************/
/********************* Forms / Buttons */
@ -388,9 +412,6 @@ form p.inputinfo {
.formpage form.pure-form input[type=submit] {
margin-top: 1.25em;
}
.formpage form.pure-form {
}
.formpage form.pure-form .inputhelp {
width: 80%;
@ -552,29 +573,70 @@ a.home-signup-button {
/********************* Account */
.account-status {
text-align: center;
margin-bottom: 2em;
padding: 0;
}
.account-status-paid, .account-status-disabled {
font-weight: bold;
}
.account-status__status {
font-weight: bold;
}
.account-status h3 {
margin-left: 2em;
margin: 1em;
}
.account-status table {
width: 100%;
}
.account-status table tr {
border-top: 1px solid #ddd;
}
.account-status table td {
padding: 0.5em 1em;
width: 50%;
}
.account-status table td:first-child {
border-right: 1px solid #ddd;
text-align: right;
}
.account-aff-box {
background: #E6F5FF;
border-radius: 4px;
border: 1px solid #72B6ED;
box-shadow: 1px 1px 3px #aaa;
padding: 0.6em 2em;
margin: 2em 0 0 0;
border: 1px solid #A8B3BA;
margin: 1em 0 0 0;
-webkit-box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
-moz-box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
}
.account-aff-box p, .account-aff-box form {
margin: 1.5em;
}
.account-aff-box fieldset {
padding: 0;
}
.account-motd {
background: #E6F5FF;
border-radius: 4px;
border: 1px solid #72B6ED;
box-shadow: 1px 1px 3px #aaa;
border: 1px solid #A8B3BA;
padding: 0.3em 2em;
text-align: center;
margin: 2em 0 0 0;
margin: 0 auto 0 auto;
-webkit-box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
-moz-box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.13);
}
.account-motd p {
margin: 0.25em 0;
}
.account-fund {
padding: 0;
}
.account-payment-box form label, .account-giftcode-box form label {
@ -607,13 +669,8 @@ a.home-signup-button {
width: auto;
}
.account-payment-tabs {
float : left;
padding: 1em;
}
.account-payment-tab > label {
width: 50%;
width: 33.32%;
text-align: center;
display: block;
float: left;
@ -622,13 +679,12 @@ a.home-signup-button {
-ms-user-select: none;
-webkit-user-select: none;
height: 2em;
border: 1px solid #ccc;
border-bottom: 0;
border-radius: 5px 5px 0px 0px;
-moz-border-radius: 5px 5px 0px 0px;
-webkit-border-radius: 5px 5px 0px 0px;
border-right: 1px solid #ccc;
background: #eee;
}
.account-payment-tab:last-child > label {
border-right: 0;
}
.account-payment-tab > label span {
display: block;
width: 100%;
@ -639,6 +695,7 @@ a.home-signup-button {
}
.account-payment-tab [id^="tab"]:checked + label span {
background: #fff;
font-weight: bold;
}
.account-payment-tab [id^="tab"]:checked + label {
background: #fff;
@ -648,7 +705,6 @@ a.home-signup-button {
}
.account-payment-tab .tab-content {
display: none;
border: 1px solid #ccc;
float: right;
width: 100%;
margin: 2em 0 0 -100%;
@ -664,6 +720,13 @@ a.home-signup-button {
color: #666;
}
.account-trial {
padding: 0;
}
.account-trial p {
margin: 1em 2em;
}
@media screen and (min-width: 64em) {
@ -830,8 +893,6 @@ div.ticket-message:last-child {
div.ticket-message-user {
background: #e8e8e8;
}
div.ticket-message-staff {
}
div.ticket-message-private {
background: #f8e7e7;
}

@ -4,14 +4,15 @@
{% load staticfiles %}
{% block content %}
<div class="flex-page">
<div class="pure-g">
<div class="pure-u-1 pure-u-xl-2-3 flex-page">
<div class="left-menu">
<div class="submenu">
<p class="menu-title">{% trans 'Account' %}</p>
<p class="menu-title"></p>
<ul>
<li><a href="/account/">
<i class="fa fa-dashboard fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Overview' %}
{% trans 'Your account' %}
</a></li>
<li><a href="/account/config">
<i class="fa fa-download fa-fw" aria-hidden="true"></i>&nbsp;
@ -23,23 +24,22 @@
{% trans 'WireGuard' %}
</a></li>
{% endif %}
<li><a href="/account/settings">
<i class="fa fa-gears fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Settings' %}
</a></li>
<li><a href="/payments">
<i class="fa fa-credit-card fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Payments' %}
</a></li>
<li><a href="/account/logs">
<i class="fa fa-archive fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Logs' %}
<li><a href="/account/settings">
<i class="fa fa-gears fa-fw" aria-hidden="true"></i>&nbsp;
{% trans 'Settings' %}
</a></li>
</ul>
</div>
</div>
{% block account_content_outer %}
<div class="content-box">
{% block account_content %}{% endblock %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}

@ -5,78 +5,59 @@
{% block headers %}
{% endblock %}
{% block account_content %}
<div>
{% block account_content_outer %}
<div class="flex-page-content">
{% if user_motd %}
<div class="account-motd">
<p> {{ user_motd | safe }} </p>
</div>
{% endif %}
<h2>{% trans 'Account' %} : {{user.username}}</h2>
<div class="content-box account-status">
<h3>{% trans 'Account' %} : {{user.username}}</h3>
<div class="account-status">
{% if subscription %}
<table>
<tr>
<td>{% trans "Status" %}</td>
<td class="account-status__status">
{% if user.vpnuser.is_paid %}
ACTIVE
{% else %}
INACTIVE
{% endif %}
</td>
</tr>
<tr>
<td>{% trans "Subscription" %}</td>
<td>
{% if subscription.status == 'active' %}
<p class="account-status-paid">
{% blocktrans trimmed with until=subscription.next_renew|date:'DATE_FORMAT' backend=subscription.backend.backend_verbose_name %}
Your account is active. Your subscription will automatically renew on {{until}} ({{backend}}).
{% blocktrans trimmed with until=subscription.next_renew|date:'Y-m-d' backend=subscription.backend.backend_verbose_name %}
<b>ACTIVE</b>. Renews on {{until}} via {{backend}}.
{% endblocktrans %}
(<a href="{% url 'payments:cancel_subscr' %}">{% trans "cancel" %}</a>)
</p>
{% else %}
<p class="account-status-paid">
{% blocktrans trimmed with backend=subscription.backend.backend_verbose_name %}
Your subscription is waiting for confirmation by {{backend}}.
It may take up to a few minutes.
{% endblocktrans %}
</p>
-
{% endif %}
{% elif user.vpnuser.is_paid %}
<p class="account-status-paid">
{% blocktrans trimmed with until=user.vpnuser.expiration|date:'DATETIME_FORMAT' %}
Your account is paid until {{until}}
{% endblocktrans %}
</td>
</tr>
<tr>
<td>{% trans "Expiration" %}</td>
<td>
{{user.vpnuser.expiration|date:'Y-m-d H:i'|default:'-'}}
{% if user.vpnuser.is_paid %}
{% blocktrans trimmed with left=user.vpnuser.expiration|timeuntil %}
({{ left }} left)
{% endblocktrans %}
</p>
{% elif user.vpnuser.can_have_trial %}
<p class="account-status-trial">
{% blocktrans trimmed with left=user.vpnuser. %}
You can activate your free trial account for two hours periods for up to one week,
by clicking this button:
{% endblocktrans %}
<form action="/account/trial" method="post" class="pure-form" name="trial_form">
{% csrf_token %}
<fieldset>
<div id="captcha"></div>
<noscript>
<input type="submit" class="pure-button pure-button-primary" value="{% trans 'Activate' %}" />
</noscript>
</fieldset>
</form>
</p>
<script type="text/javascript">
var captchaLoadCallback = function() {
grecaptcha.render("captcha", {
"sitekey": "{{ recaptcha_site_key }}",
"callback": function(response) { document.trial_form.submit(); },
});
};
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=captchaLoadCallback&render=explicit"
async defer></script>
{% else %}
<p class="account-status-disabled">{% trans 'Your account is not paid.' %}</p>
{% endif %}
</td>
</tr>
</table>
</div>
{% if not subscription %}
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-2 account-payment-box account-payment-tabs">
<div class="content-box account-fund">
<div class="account-payment-box account-payment-tabs">
<div class="account-payment-tab">
<input type="radio" name="type" id="tab_subscr" value="subscr" checked />
<label for="tab_subscr"><span>{% trans 'Subscription' %}</span></label>
@ -154,24 +135,52 @@
</form>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-lg-1-2 account-giftcode-box">
<div class="account-payment-tab">
<input type="radio" name="type" id="tab_coupon" value="coupon" />
<label for="tab_coupon"><span>{% trans 'Coupon' %}</span></label>
<div class="tab-content">
<form action="/account/gift_code" method="post" class="pure-form pure-form-aligned centered-form">
{% csrf_token %}
<fieldset>
<div class="pure-control-group">
<label for="ins_code">{% trans 'Gift code' %}</label>
<input type="text" id="ins_code" name="code" maxlength="32"
<label for="ins_code">{% trans 'Code' %}</label>
<input type="text" id="ins_code" name="code" maxlength="32" class="pure-input-1-2"
pattern="[ ]*[a-zA-Z0-9]{1,32}[ ]*" autocomplete="off" />
</div>
<div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary"
value="{% trans 'Redeem gift code' %}" />
value="{% trans 'Use' %}" />
</div>
<p>{% trans 'Our coupons are alphanumeric codes that give you a fixed duration of VPN access.' %}<br />
{% trans 'They can be single or multi use.' %}
</p>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% if user.vpnuser.can_have_trial %}
<div class="content-box account-trial">
<p>
{% blocktrans trimmed with left=user.vpnuser. %}
For up to one week, you can activate your free trial for the next two hours by clicking on this button:
{% endblocktrans %}
</p>
<form action="/account/trial" method="post" class="pure-form" name="trial_form">
{% csrf_token %}
<div class="h-captcha" data-sitekey="{{ hcaptcha_site_key }}" data-callback="onCaptcha"></div>
<script src="https://hcaptcha.com/1/api.js" async defer></script>
<script>
function onCaptcha(v) {
document.forms.trial_form.submit();
}
</script>
</form>
</div>
{% endif %}
<div class="account-aff-box">
@ -190,8 +199,8 @@
</form>
</p>
</div>
</div>
</div>
{% endblock %}

@ -1,54 +0,0 @@
{% extends 'account_layout.html' %}
{% load i18n %}
{% load staticfiles %}
{% block account_content %}
<h2>{% trans 'Logs' %}</h2>
<p>{% trans 'Everything we have to keep about you. Automatically deleted after 1 year.' %}</p>
<table class="admin-list">
<thead>
<tr>
<td>{% trans 'Date' %}</td>
<td>{% trans 'Duration' %}</td>
<td>{% trans 'Client IP' %}</td>
<td>{% trans 'Shared IP' %}</td>
<td>{% trans 'Bandwidth' %}</td>
</tr>
</thead>
<tbody>
{% for line in sessions %}
<tr>
<td>{{ line.connect_date }}</td>
<td>
{% if line.disconnect_date != None %}
{{ line.connect_date|timesince:line.disconnect_date }}
{% else %}
<b>{% trans "Open" %}</b>
{% endif %}
</td>
<td>{{ line.remote.addr|default:_('[unknown]') }}</td>
<td>{{ line.gateway.main_addr.ipv4|default:_('[unknown]') }}</td>
<td>{{ line.stats.up | filesizeformat }} /
{{ line.stats.down | filesizeformat }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="pages">
{% if prev != None and prev > 0 %}
<a href="?page=0">&lt;&lt;</a>
{% endif %}
{% if prev != None %}
<a href="?page={{ prev }}">&lt;</a>
{% endif %}
<a href="?page={{ page }}">{{ page }}</a>
{% if next != None %}
<a href="?page={{ next }}">&gt;</a>
{% endif %}
{% if next != None and last_page > next %}
<a href="?page={{ last_page }}">&gt;&gt;</a>
{% endif %}
</p>
{% endblock %}

@ -2,25 +2,55 @@
{% load i18n %}
{% load staticfiles %}
{% block account_content %}
{% block account_content_outer %}
<div class="flex-page-content account-settings">
<h2>{% trans 'Settings' %}</h2>
<div class="content-box">
<h3>{% trans 'Change e-mail address' %}</h3>
<p>{% trans "Current address" %}: {{user.email|default:"none"}}</p>
<form action="/account/settings" method="post" class="pure-form pure-form-aligned">
{% csrf_token %}
<input type="hidden" name="action" value="email" />
<fieldset>
<div class="pure-control-group">
<label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="password" id="ins_curpassword" name="current_password" class="pure-input-1-2" />
</div>
<div class="pure-control-group">
<label for="ins_email">{% trans 'New E-Mail' %}</label>
<input type="email" id="ins_email" name="email" class="pure-input-1-2" />
</div>
<div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary" value="{% trans 'Save' %}" />
</div>
</fieldset>
</form>
</div>
<div class="content-box">
<h3>{% trans 'Change password' %}</h3>
<form action="/account/settings" method="post" class="pure-form pure-form-aligned">
{% csrf_token %}
<input type="hidden" name="action" value="password" />
<fieldset>
<div class="pure-control-group">
<label for="ins_password">{% trans 'Change password' %}</label>
<input type="password" id="ins_password" autocomplete="off" name="password" />
<label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="password" id="ins_curpassword" name="current_password" class="pure-input-1-2" />
</div>
<div class="pure-control-group">
<label for="ins_password2">{% trans 'Change password' %} ({% trans 'repeat' %})</label>
<input type="password" id="ins_password2" autocomplete="off" name="password2" />
<label for="ins_password">{% trans 'New password' %}</label>
<input type="password" id="ins_password" name="password" class="pure-input-1-2" />
</div>
<div class="pure-control-group">
<label for="ins_email">{% trans 'E-Mail' %}</label>
<input type="email" id="ins_email" name="email" autocomplete="off" value="{{user.email}}" />
<label for="ins_password2">{% trans 'New password' %} ({% trans 'repeat' %})</label>
<input type="password" id="ins_password2" name="password2" class="pure-input-1-2" />
</div>
<div class="pure-controls">
@ -28,4 +58,64 @@
</div>
</fieldset>
</form>
</div>
<div class="content-box">
<h3>{% trans 'Data export' %}</h3>
<form action="/account/settings" method="post" class="pure-form pure-form-aligned">
{% csrf_token %}
<input type="hidden" name="action" value="export" />
<fieldset>
<div class="pure-control-group">
<label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="password" id="ins_curpassword" name="current_password" />
</div>
<p>
{% blocktrans trimmed %}
Produces an archive containing account details and the logs we legally have to keep for one year.
{% endblocktrans %}
</p>
<div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary button-danger" value="{% trans 'Export my data' %}" />
</div>
<p class="inputinfo">
{% blocktrans trimmed %}
It may take a few seconds.
{% endblocktrans %}
</p>
</fieldset>
</form>
</div>
<div class="content-box">
<h3>{% trans 'Account deletion' %}</h3>
<form action="/account/settings" method="post" class="pure-form pure-form-aligned">
{% csrf_token %}
<input type="hidden" name="action" value="delete" />
<fieldset>
{% if not can_delete %}
<p>
{% blocktrans trimmed %}
You need to cancel your active subscription before deleting your account.
{% endblocktrans %}
</p>
{% else %}
<div class="pure-control-group">
<label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="password" id="ins_curpassword" name="current_password" />
</div>
<div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary button-danger" value="{% trans 'Delete my account' %}" />
</div>
{% endif %}
</fieldset>
</form>
</div>
</div>
{% endblock %}

@ -12,8 +12,7 @@
{% csrf_token %}
<fieldset>
<p>
{% trans "We're sorry to see you go." %}<br />
{% trans "Would you like to tell us why, or leave any feedback so we can improve our service?" %}
{% trans "Would you like to tell us why, or leave any feedback so we can improve our service? It's optional." %}
</p>
<textarea name="feedback" class="pure-input-1" maxlength=10000></textarea>

Loading…
Cancel
Save