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_SITE_KEY = ''
RECAPTCHA_SECRET_KEY = '' RECAPTCHA_SECRET_KEY = ''
HCAPTCHA_API = 'https://hcaptcha.com/siteverify'
HCAPTCHA_SITE_KEY = ''
HCAPTCHA_SECRET_KEY = ''
# lcore API settings # lcore API settings
LCORE = dict( LCORE = dict(
BASE_URL='https://core.test.lambdavpn.net/v1/', BASE_URL='https://core.test.lambdavpn.net/v1/',

@ -41,6 +41,15 @@ class VPNUser(models.Model, LcoreUserProfileMethods):
referrer_used = models.BooleanField(default=False) referrer_used = models.BooleanField(default=False)
campaign = models.CharField(blank=True, null=True, max_length=64) 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): def give_trial_period(self):
self.add_paid_time(get_trial_period_duration()) self.add_paid_time(get_trial_period_duration())
self.trial_periods_given += 1 self.trial_periods_given += 1

@ -12,12 +12,11 @@ urlpatterns = [
path('logout', views.logout, name='logout'), path('logout', views.logout, name='logout'),
path('signup', views.signup, name='signup'), path('signup', views.signup, name='signup'),
path('settings', views.settings), path('settings', views.settings, name='account_settings'),
path('config', views.config), path('config', views.config),
path('config_dl', django_lcore.views.openvpn_dl), path('config_dl', django_lcore.views.openvpn_dl),
path('wireguard', views.wireguard), path('wireguard', views.wireguard),
path('wireguard/new', views.wireguard_new, name='wireguard_new'), path('wireguard/new', views.wireguard_new, name='wireguard_new'),
path('logs', views.logs),
path('gift_code', views.gift_code), path('gift_code', views.gift_code),
path('trial', views.trial), path('trial', views.trial),
path('', views.index, name='index'), 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.decorators import login_required, user_passes_test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Count from django.db.models import Count
from django.db import transaction
from django.http import (HttpResponse, from django.http import (HttpResponse,
HttpResponseNotFound, HttpResponseRedirect, HttpResponseNotFound, HttpResponseRedirect,
JsonResponse) JsonResponse)
@ -177,7 +178,7 @@ def index(request):
if b.backend_has_recurring), if b.backend_has_recurring),
key=lambda x: x.backend_id), key=lambda x: x.backend_id),
default_backend='paypal', default_backend='paypal',
recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY, hcaptcha_site_key=project_settings.HCAPTCHA_SITE_KEY,
price=price_fn(), price=price_fn(),
user_motd=site_config.MOTD_USER, user_motd=site_config.MOTD_USER,
) )
@ -185,14 +186,13 @@ def index(request):
def captcha_test(grr, 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': if api_url == 'TEST' and grr == 'TEST-TOKEN':
# FIXME: i'm sorry. # FIXME: i'm sorry.
return True return True
data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY, data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY,
remoteip=get_client_ip(request),
response=grr) response=grr)
try: try:
@ -220,30 +220,170 @@ def trial(request):
return redirect('account:index') 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 @login_required
def settings(request): def settings(request):
can_delete = request.user.vpnuser.get_subscription() is None
if request.method != 'POST': 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') action = request.POST.get('action')
pw2 = request.POST.get('password2')
if pw and pw2: current_pw = request.POST.get('current_password')
if pw != pw2: if not request.user.check_password(current_pw):
messages.error(request, _("Passwords do not match")) messages.error(request, _("Invalid password"))
else: return redirect('lambdainst:account_settings')
request.user.set_password(pw)
django_lcore.sync_user(request.user.vpnuser)
messages.success(request, _("OK!"))
if action == 'email':
email = request.POST.get('email') email = request.POST.get('email')
if email: if email:
request.user.email = email request.user.email = email
messages.success(request, _("OK! Email address changed."))
else: else:
request.user.email = '' request.user.email = ''
messages.success(request, _("OK! Email address unset."))
request.user.save() 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 @login_required
@ -263,31 +403,6 @@ def gift_code(request):
return redirect('account:index') 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 @login_required
def config(request): def config(request):
return render(request, 'lambdainst/config.html', dict( return render(request, 'lambdainst/config.html', dict(

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

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

@ -5,78 +5,59 @@
{% block headers %} {% block headers %}
{% endblock %} {% endblock %}
{% block account_content %} {% block account_content_outer %}
<div> <div class="flex-page-content">
{% if user_motd %} {% if user_motd %}
<div class="account-motd"> <div class="account-motd">
<p> {{ user_motd | safe }} </p> <p> {{ user_motd | safe }} </p>
</div> </div>
{% endif %} {% endif %}
<h2>{% trans 'Account' %} : {{user.username}}</h2> <div class="content-box account-status">
<h3>{% trans 'Account' %} : {{user.username}}</h3>
<div class="account-status"> <table>
{% if subscription %} <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' %} {% if subscription.status == 'active' %}
<p class="account-status-paid"> {% blocktrans trimmed with until=subscription.next_renew|date:'Y-m-d' backend=subscription.backend.backend_verbose_name %}
{% blocktrans trimmed with until=subscription.next_renew|date:'DATE_FORMAT' backend=subscription.backend.backend_verbose_name %} <b>ACTIVE</b>. Renews on {{until}} via {{backend}}.
Your account is active. Your subscription will automatically renew on {{until}} ({{backend}}).
{% endblocktrans %} {% endblocktrans %}
(<a href="{% url 'payments:cancel_subscr' %}">{% trans "cancel" %}</a>) (<a href="{% url 'payments:cancel_subscr' %}">{% trans "cancel" %}</a>)
</p>
{% else %} {% 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 %} {% endif %}
{% elif user.vpnuser.is_paid %} </td>
<p class="account-status-paid"> </tr>
{% blocktrans trimmed with until=user.vpnuser.expiration|date:'DATETIME_FORMAT' %} <tr>
Your account is paid until {{until}} <td>{% trans "Expiration" %}</td>
{% endblocktrans %} <td>
{{user.vpnuser.expiration|date:'Y-m-d H:i'|default:'-'}}
{% if user.vpnuser.is_paid %}
{% blocktrans trimmed with left=user.vpnuser.expiration|timeuntil %} {% blocktrans trimmed with left=user.vpnuser.expiration|timeuntil %}
({{ left }} left) ({{ left }} left)
{% endblocktrans %} {% 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 %} {% endif %}
</td>
</tr>
</table>
</div> </div>
{% if not subscription %} {% if not subscription %}
<div class="pure-g"> <div class="content-box account-fund">
<div class="pure-u-1 pure-u-lg-1-2 account-payment-box account-payment-tabs"> <div class="account-payment-box account-payment-tabs">
<div class="account-payment-tab"> <div class="account-payment-tab">
<input type="radio" name="type" id="tab_subscr" value="subscr" checked /> <input type="radio" name="type" id="tab_subscr" value="subscr" checked />
<label for="tab_subscr"><span>{% trans 'Subscription' %}</span></label> <label for="tab_subscr"><span>{% trans 'Subscription' %}</span></label>
@ -154,24 +135,52 @@
</form> </form>
</div> </div>
</div> </div>
</div> <div class="account-payment-tab">
<div class="pure-u-1 pure-u-lg-1-2 account-giftcode-box"> <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"> <form action="/account/gift_code" method="post" class="pure-form pure-form-aligned centered-form">
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="ins_code">{% trans 'Gift code' %}</label> <label for="ins_code">{% trans 'Code' %}</label>
<input type="text" id="ins_code" name="code" maxlength="32" <input type="text" id="ins_code" name="code" maxlength="32" class="pure-input-1-2"
pattern="[ ]*[a-zA-Z0-9]{1,32}[ ]*" autocomplete="off" /> pattern="[ ]*[a-zA-Z0-9]{1,32}[ ]*" autocomplete="off" />
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary" <input type="submit" class="pure-button pure-button-primary"
value="{% trans 'Redeem gift code' %}" /> value="{% trans 'Use' %}" />
</div> </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> </fieldset>
</form> </form>
</div> </div>
</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 %} {% endif %}
<div class="account-aff-box"> <div class="account-aff-box">
@ -190,8 +199,8 @@
</form> </form>
</p> </p>
</div> </div>
</div>
</div>
{% endblock %} {% 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,30 +2,120 @@
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
{% block account_content %} {% block account_content_outer %}
<h2>{% trans 'Settings' %}</h2> <div class="flex-page-content account-settings">
<h2>{% trans 'Settings' %}</h2>
<form action="/account/settings" method="post" class="pure-form pure-form-aligned"> <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 %} {% csrf_token %}
<input type="hidden" name="action" value="email" />
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="ins_password">{% trans 'Change password' %}</label> <label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="password" id="ins_password" autocomplete="off" name="password" /> <input type="password" id="ins_curpassword" name="current_password" class="pure-input-1-2" />
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="ins_password2">{% trans 'Change password' %} ({% trans 'repeat' %})</label> <label for="ins_email">{% trans 'New E-Mail' %}</label>
<input type="password" id="ins_password2" autocomplete="off" name="password2" /> <input type="email" id="ins_email" name="email" class="pure-input-1-2" />
</div> </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"> <div class="pure-control-group">
<label for="ins_email">{% trans 'E-Mail' %}</label> <label for="ins_curpassword">{% trans 'Current password' %}</label>
<input type="email" id="ins_email" name="email" autocomplete="off" value="{{user.email}}" /> <input type="password" id="ins_curpassword" name="current_password" class="pure-input-1-2" />
</div>
<div class="pure-control-group">
<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_password2">{% trans 'New password' %} ({% trans 'repeat' %})</label>
<input type="password" id="ins_password2" name="password2" class="pure-input-1-2" />
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<input type="submit" class="pure-button pure-button-primary" value="{% trans 'Save' %}" /> <input type="submit" class="pure-button pure-button-primary" value="{% trans 'Save' %}" />
</div> </div>
</fieldset> </fieldset>
</form> </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 %} {% endblock %}

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

Loading…
Cancel
Save