diff --git a/ccvpn/settings.py b/ccvpn/settings.py
index 2132ea1..690ee09 100644
--- a/ccvpn/settings.py
+++ b/ccvpn/settings.py
@@ -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/',
diff --git a/lambdainst/models.py b/lambdainst/models.py
index 8472110..d30852a 100644
--- a/lambdainst/models.py
+++ b/lambdainst/models.py
@@ -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
diff --git a/lambdainst/urls.py b/lambdainst/urls.py
index 1fe8162..4daebfc 100644
--- a/lambdainst/urls.py
+++ b/lambdainst/urls.py
@@ -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'),
diff --git a/lambdainst/views.py b/lambdainst/views.py
index 58cb419..f076aeb 100644
--- a/lambdainst/views.py
+++ b/lambdainst/views.py
@@ -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,
+ ))
+
+ 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')
- pw = request.POST.get('password')
- pw2 = request.POST.get('password2')
- if pw and pw2:
- if pw != pw2:
- messages.error(request, _("Passwords do not match"))
+ 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!"))
-
- email = request.POST.get('email')
- if email:
- request.user.email = email
- else:
- request.user.email = ''
-
- request.user.save()
-
- return render(request, 'lambdainst/settings.html', dict(title=_("Settings")))
+ 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(
diff --git a/static/css/style.css b/static/css/style.css
index 8d03622..d48240d 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -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;
}
diff --git a/templates/account_layout.html b/templates/account_layout.html
index ee1a39c..c55364d 100644
--- a/templates/account_layout.html
+++ b/templates/account_layout.html
@@ -4,14 +4,15 @@
{% load staticfiles %}
{% block content %}
-
+
+
+ {% block account_content_outer %}
{% block account_content %}{% endblock %}
+ {% endblock %}
+
{% endblock %}
diff --git a/templates/lambdainst/account.html b/templates/lambdainst/account.html
index fd7e4f1..c992e78 100644
--- a/templates/lambdainst/account.html
+++ b/templates/lambdainst/account.html
@@ -5,78 +5,59 @@
{% block headers %}
{% endblock %}
-{% block account_content %}
-
-
+{% block account_content_outer %}
+
{% if user_motd %}
{% endif %}
-
{% trans 'Account' %} : {{user.username}}
-
-
- {% if subscription %}
- {% if subscription.status == 'active' %}
-
- {% 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}}).
- {% endblocktrans %}
- ({% trans "cancel" %} )
-
- {% else %}
-
- {% 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 %}
-
- {% endif %}
- {% elif user.vpnuser.is_paid %}
-
- {% blocktrans trimmed with until=user.vpnuser.expiration|date:'DATETIME_FORMAT' %}
- Your account is paid until {{until}}
- {% endblocktrans %}
- {% blocktrans trimmed with left=user.vpnuser.expiration|timeuntil %}
- ({{ left }} left)
- {% endblocktrans %}
-
- {% elif user.vpnuser.can_have_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 %}
-
-
-
-
-
- {% else %}
-
{% trans 'Your account is not paid.' %}
- {% endif %}
+
+
{% trans 'Account' %} : {{user.username}}
+
+
+
+ {% trans "Status" %}
+
+ {% if user.vpnuser.is_paid %}
+ ACTIVE
+ {% else %}
+ INACTIVE
+ {% endif %}
+
+
+
+ {% trans "Subscription" %}
+
+ {% if subscription.status == 'active' %}
+ {% blocktrans trimmed with until=subscription.next_renew|date:'Y-m-d' backend=subscription.backend.backend_verbose_name %}
+ ACTIVE . Renews on {{until}} via {{backend}}.
+ {% endblocktrans %}
+ ({% trans "cancel" %} )
+ {% else %}
+ -
+ {% endif %}
+
+
+
+ {% trans "Expiration" %}
+
+ {{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 %}
+ {% endif %}
+
+
+
+
{% if not subscription %}
-
-
+
-
-
+
+
+
{% trans 'Coupon' %}
+
+
{% endif %}
+ {% if user.vpnuser.can_have_trial %}
+
+
+ {% 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 %}
+
+
+
+ {% endif %}
+
{% blocktrans trimmed %}
@@ -190,8 +199,8 @@
-
+
{% endblock %}
diff --git a/templates/lambdainst/logs.html b/templates/lambdainst/logs.html
deleted file mode 100644
index 8d75b87..0000000
--- a/templates/lambdainst/logs.html
+++ /dev/null
@@ -1,54 +0,0 @@
-{% extends 'account_layout.html' %}
-{% load i18n %}
-{% load staticfiles %}
-
-{% block account_content %}
-
{% trans 'Logs' %}
-
{% trans 'Everything we have to keep about you. Automatically deleted after 1 year.' %}
-
-
-
-
- {% trans 'Date' %}
- {% trans 'Duration' %}
- {% trans 'Client IP' %}
- {% trans 'Shared IP' %}
- {% trans 'Bandwidth' %}
-
-
-
- {% for line in sessions %}
-
- {{ line.connect_date }}
-
- {% if line.disconnect_date != None %}
- {{ line.connect_date|timesince:line.disconnect_date }}
- {% else %}
- {% trans "Open" %}
- {% endif %}
-
- {{ line.remote.addr|default:_('[unknown]') }}
- {{ line.gateway.main_addr.ipv4|default:_('[unknown]') }}
- {{ line.stats.up | filesizeformat }} /
- {{ line.stats.down | filesizeformat }}
-
-
- {% endfor %}
-
-
-
- {% if prev != None and prev > 0 %}
- <<
- {% endif %}
- {% if prev != None %}
- <
- {% endif %}
- {{ page }}
- {% if next != None %}
- >
- {% endif %}
- {% if next != None and last_page > next %}
- >>
- {% endif %}
-
-{% endblock %}
diff --git a/templates/lambdainst/settings.html b/templates/lambdainst/settings.html
index 84b39e7..0163e7e 100644
--- a/templates/lambdainst/settings.html
+++ b/templates/lambdainst/settings.html
@@ -2,30 +2,120 @@
{% load i18n %}
{% load staticfiles %}
-{% block account_content %}
-
{% trans 'Settings' %}
-
-
+{% block account_content_outer %}
+
+
{% trans 'Settings' %}
+
+
+
{% trans 'Change e-mail address' %}
+
+
{% trans "Current address" %}: {{user.email|default:"none"}}
+
+
+
+
+
{% trans 'Change password' %}
+
+
+
+
+
{% trans 'Data export' %}
+
+
+
+
+
+
{% trans 'Account deletion' %}
+
+
+
+
+
+
+
{% endblock %}
diff --git a/templates/payments/cancel_subscr.html b/templates/payments/cancel_subscr.html
index b42cadc..d32ab85 100644
--- a/templates/payments/cancel_subscr.html
+++ b/templates/payments/cancel_subscr.html
@@ -12,8 +12,7 @@
{% csrf_token %}
- {% trans "We're sorry to see you go." %}
- {% 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." %}