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 %} -
+
+
{% 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}}

- - {% 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.' %}

- - - - - - - - - - - - - {% for line in sessions %} - - - - - - - - {% endfor %} - -
{% trans 'Date' %}{% trans 'Duration' %}{% trans 'Client IP' %}{% trans 'Shared IP' %}{% trans 'Bandwidth' %}
{{ 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 }} -
-

- {% 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' %}

- -
- {% csrf_token %} -
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
+{% block account_content_outer %} + + + + {% 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." %}