...
 
Commits (113)
......@@ -2,7 +2,7 @@
Keel (LDH middleware)
<https://source.puri.sm/liberty/ldh_middleware>
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
......
......@@ -20,37 +20,20 @@ python-decouple = "==3.1"
python-ldap = "==3.0.0"
strictyaml = "==0.11.10"
confusable_homoglyphs = "==3.0.0"
Django = "==1.11.18"
Django = "==1.11.20"
django_celery_beat = "==1.1.0"
Jinja2 = "==2.10"
WooCommerce = "==1.2.1"
django-simple-captcha = "==0.5.10"
djangorestframework = "*"
djangorestframework = "==3.9.2"
django-password-reset = "==2.0"
psycopg2-binary = "==2.8.2"
[dev-packages]
# self:
# purist_middleware = {path = ".", editable = true}
# Documentation depencencies:
# alabaster==0.7.10
# Babel==2.5.3
# certifi==2017.11.5
# chardet==3.0.4
# docutils==0.14
# idna==2.6
# imagesize==0.7.1
# Jinja2==2.10
# MarkupSafe==1.0
# Pygments==2.2.0
# pytz==2017.3
# requests==2.18.4
# six==1.11.0
# snowballstemmer==1.2.1
# Sphinx==1.6.6
# sphinxcontrib-websupport==1.0.1
# urllib3==1.22
twine = "==1.12.1"
pylint = "*"
django_extensions = "*"
pydotplus = "*"
[requires]
python_version = "3"
......@@ -35,9 +35,12 @@ Models
Model diagram generated with:
./manage.py graph_models --all-applications --group-models \
./ldh_middleware graph_models --all-applications --group-models \
--verbose-name --output models.png
Requires GraphViz (`apt install graphviz`) and dev dependencies
(`pipenv install --dev`).
Build wheel package (and optionally upload)
-------------------------------------------
......@@ -127,7 +130,7 @@ Sharing and contributions
Keel (LDH middleware)
<https://source.puri.sm/liberty/ldh_middleware>
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
SPDX-License-Identifier: AGPL-3.0-or-later
Shared under AGPL-3.0-or-later. We adhere to the Community Covenant
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class BundlechangeConfig(AppConfig):
name = 'bundlechange'
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from purist.forms import ConfirmAddressForm
class ChooseBundleForm(ConfirmAddressForm):
def __init__(self, choices=[(1, 'NoConf')], *args, **kwargs):
super(ChooseBundleForm, self).__init__(*args, **kwargs)
self.fields['bundle'] = forms.ChoiceField(
label=_('You can change to the following bundles'),
widget=forms.RadioSelect,
choices=choices
)
self.fields['address'].label = _(
'Type in your {} address to confirm the change'.format(
settings.SITE_TITLE))
self.order_fields(('bundle', 'address'))
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-12 12:54
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BundleChange',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('from_bdl', models.CharField(max_length=100)),
('to_bdl', models.CharField(max_length=100)),
('is_pending', models.BooleanField(default=True)),
('pay_url', models.URLField(default='')),
('new_order_id', models.CharField(max_length=100)),
('old_subscription_id', models.CharField(max_length=100)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
from django.db import models
from django.conf import settings
from django.utils import timezone
from limitmonitor.models import get_available_bundles
class BundleChange(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
from_bdl = models.CharField(max_length=100)
to_bdl = models.CharField(max_length=100)
is_pending = models.BooleanField(default=True)
pay_url = models.URLField(default='')
new_order_id = models.CharField(max_length=100)
old_subscription_id = models.CharField(max_length=100)
created_date = models.DateTimeField(default=timezone.now)
def is_family_downgrade(self):
"""Returns true if this BundleChange instance is a downgrade from a
group bundle (Family pack) to a single bundle (Basic or
Complete). Returns False otherwise."""
bundles = get_available_bundles()
from_bdl = bundles.get(self.from_bdl, None)
to_bdl = bundles.get(self.to_bdl, None)
if from_bdl and to_bdl:
if (
from_bdl.get('type', None) == 'GROUP'
and (
to_bdl.get('type', None) == 'COMPLETE'
or to_bdl.get('type', None) == 'BASIC'
)
):
return True
return False
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% trans 'Manage Upgrades' %}{% endblock %}
{% block extra_css%}{% include "purist/spinner.html" %}{% endblock %}
{% block header %}{% trans 'Manage Upgrades' %}{% endblock %}
{% block byline %}
{% if can_upgrade %}
{% trans 'You are currently on' %} <strong>{{ current_bdl }}</strong> {% trans 'bundle.' %}
{% endif %}
{% endblock %}
{% block content %}
{% if has_pending_changes %}
<p>{% trans 'Your change from ' %} <strong>{{ from_bdl }}</strong> {% trans ' to ' %} <strong>{{ to_bdl }}</strong> {% trans 'was successful.' %}<br>
Use <a href="{{ pay_url }}">this link</a> to pay.</p>
<p>{% trans 'After payment is done your account will be updated in few minutes.' %}
{% elif can_upgrade %}
{% if not invitees_clean %}
<p> {% trans 'You cannot downgrade while one or more invitees are missing a recovery address. Please prompt them to update their details or contact support.' %}</p>
{% else %}
<form id="change_bundle" action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm" />
<a href="{% url 'profile' %}" class="button">No thanks</a>
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endif %}
{% else %}
{% trans 'There is no upgrade or downgrade options for your account.' %}
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
const frm = document.getElementById('change_bundle'),
spin = document.getElementById('spinner-wrap');
frm.addEventListener('submit', enable_spinner);
function enable_spinner() {
spin.setAttribute('class', 'spinner-wrapper');
}
</script>
{% endblock %}
from django.test import TestCase
# Create your tests here.
from django.urls import reverse_lazy
from django.conf import settings
from django.views.generic.edit import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from .forms import ChooseBundleForm
from .models import BundleChange
from limitmonitor.common import parse_subscription, set_subscription_status, \
SubscriptionParseError
from limitmonitor.models import get_available_bundles
from limitmonitor.task_resources.subscription import upgrade_user_subscription
class UpgradeView(LoginRequiredMixin, FormView):
template_name = 'bundlechange/upgrade.html'
form_class = ChooseBundleForm
success_url = reverse_lazy('upgrade')
def get_form_kwargs(self):
choices = self.request.user.get_upgrade_choices(add_downgrade=True)
kwargs = super(UpgradeView, self).get_form_kwargs()
kwargs['choices'] = choices
return kwargs
def get_form(self, *args, **kwargs):
form = super(UpgradeView, self).get_form(*args, **kwargs)
form.user = self.request.user
return form
def get_context_data(self, **kwargs):
context = super(UpgradeView, self).get_context_data(**kwargs)
pending_changes = self.request.user.bundlechange_set.filter(
is_pending=True
)
avl_bundles = get_available_bundles()
u_ec = self.request.user.get_external_credit()
if u_ec is not None:
context['current_bdl'] = (
avl_bundles[u_ec.bundle_key]['short_title'])
if len(pending_changes) > 0:
context['has_pending_changes'] = True
context['from_bdl'] = (
avl_bundles[pending_changes[0].from_bdl]['short_title'])
context['to_bdl'] = (
avl_bundles[pending_changes[0].to_bdl]['short_title'])
context['pay_url'] = pending_changes[0].pay_url
else:
context['has_pending_changes'] = False
context['can_upgrade'] = self.request.user.can_change_bundle()
context['invitees_clean'] = self.request.user.invitees_clean()
return context
def form_valid(self, form):
variation_id = form.cleaned_data['bundle']
subs = self.request.user.get_woocommerce_subscriptions()
# We assume the user has only 1 active subscription!
if subs:
try:
parsed_sub = parse_subscription(subs[0])
except SubscriptionParseError as e:
print('Error processing bundle_change for user {}: {}'.format(
self.request.user, repr(e)))
return super(UpgradeView, self).form_valid(form)
order = upgrade_user_subscription(self.request.user,
variation_id)
pay_url = settings.WOO_UPGRADE_PAY_URL.format(order['id'],
order['key'])
bundle_change = BundleChange(
user=self.request.user,
from_bdl=parsed_sub['bundle_key'],
to_bdl=variation_id,
is_pending=True,
pay_url=pay_url,
new_order_id=order['id'],
old_subscription_id=parsed_sub['external_key']
)
bundle_change.save()
set_subscription_status(parsed_sub['external_key'],
'pending-cancel')
return super(UpgradeView, self).form_valid(form)
from django.contrib import admin
from .models import ChosenReward
# Register your models here.
class ChosenRewardAdmin(admin.ModelAdmin):
list_display = ['user', 'reward', 'is_pending']
admin.site.register(ChosenReward, ChosenRewardAdmin)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-02 09:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cart', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='chosenreward',
name='is_pending',
field=models.BooleanField(default=True),
),
]
from django.db import models
from django.contrib.auth import get_user_model
from django.conf import settings
User = get_user_model()
# Create your models here.
class ChosenReward(models.Model):
user = models.OneToOneField(User)
user = models.OneToOneField(settings.AUTH_USER_MODEL)
reward = models.IntegerField()
is_pending = models.BooleanField(default=True)
......@@ -11,10 +11,111 @@
{% endif %}
{% endblock %}
{% block extra_css %}
<style>
.wait {
display: block;
text-align: center;
font-size: 5em;
}
.hidden {
display: none;
}
.spinner-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background: white;
}
.spinner {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border: 10px transparent solid;
border-top-color:black;
border-radius: 50%;
border-bottom: 10px transparent solid;
z-index: 1500;
/* Animation stuff below */
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.spinner:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border: 10px transparent solid;
border-top-color:black;
border-radius: 50%;
/* Animation stuff below */
-webkit-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg); /* IE 9 */
transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg); /* IE 9 */
transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg); /* IE 9 */
transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg); /* IE 9 */
transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */
}
}
</style>
{% endblock extra_css %}
{% block content %}
<form method="post" action=".">
<form id="register-form" method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans 'Submit' %}" />
{% if is_billing %}
<input type="submit" value="{% trans 'Billing >>' %}" />
{% else %}
<input type="submit" value="{% trans 'Register >>' %}" />
{% endif %}
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endblock %}
{% block extra_js %}
<script>
const frm = document.getElementById('register-form'),
spin = document.getElementById('spinner-wrap');
frm.addEventListener('submit', enable_spinner);
function enable_spinner() {
spin.setAttribute('class', 'spinner-wrapper');
}
</script>
{% endblock extra_js %}
\ No newline at end of file
from captcha.fields import CaptchaField
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from registration.backends.simple.views import RegistrationView
from registration.forms import validators
from captcha.fields import CaptchaField
from cart.models import ChosenReward
from ldapregister.forms import RegistrationForm
from registration.backends.simple.views import RegistrationView
from purist.models import AccountType, get_woo_connection
User = get_user_model()
def validate_reserved_names(value):
# premature optimization is the root of all evil
for bad_word in settings.REG_BAD_SUBSTRINGS:
if bad_word in value:
raise ValidationError(validators.RESERVED_NAME, code='invalid')
def woo_email_available_validator(value):
wc = get_woo_connection()
result = wc.get('customers?email={}'.format(value))
if result.ok and len(result.json()) > 0:
raise ValidationError(_('This recovery email is already in use.'))
class CartRegistrationForm(RegistrationForm):
captcha = CaptchaField()
class Meta(RegistrationForm.Meta):
fields = (
User.USERNAME_FIELD,
'password1',
'password2',
'email'
)
field_order = ('username', 'email', 'password1', 'password2', 'captcha')
email = forms.EmailField(
label=_('Recovery email address'),
help_text=_('Enter an email address where we can send you '
'recovery information.'),
validators=[
validators.validate_confusables_email, woo_email_available_validator
],
)
password1 = forms.CharField(
label=_("Passphrase"),
strip=False,
widget=forms.PasswordInput,
help_text=_('A good passphrase is made of at least three long words.'),
validators=[
RegexValidator(
regex=r'[\\\'\"]',
message=_('\\ \' and \" are not valid characters'),
code=None,
inverse_match=True,
flags=0),
],
)
def __init__(self, *args, **kwargs):
super(CartRegistrationForm, self).__init__(*args, **kwargs)
self.fields['username'].validators.append(validate_reserved_names)
def clean_email(self):
return self.cleaned_data['email'].lower()
class CartRegistrationFormWithCaptcha(CartRegistrationForm):
captcha = CaptchaField(
label=_('Please solve this math problem'),
help_text=_('Prove you are not a robot. Solve this addition, subtraction or multiplication problem.'),
)
class CartRegistrationView(RegistrationView):
form_class = CartRegistrationForm
form_class = CartRegistrationFormWithCaptcha
template_name = 'cart/registration_form.html'
reward = None
def get_form_class(self):
if settings.DEBUG_REMOVE_CAPTCHA:
return CartRegistrationForm
return self.form_class
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['DEBUG_REGISTER_STATUS'] = settings.DEBUG_REGISTER_STATUS
is_billing = False
billing_carts = list(
set().union(settings.WOO_CART_BASIC,
settings.WOO_CART_COMPLETE,
settings.WOO_CART_GROUP_BASIC,
settings.WOO_CART_GROUP_COMPLETE)
)
if self.reward in billing_carts:
is_billing = True
context['is_billing'] = is_billing
return context
def get(self, *args, **kwargs):
logout(self.request)
valid_cart_numbers = list(set().union(settings.WOO_CART_BASIC, settings.WOO_CART_COMPLETE))
valid_cart_numbers = list(
set().union(settings.WOO_CART_BASIC,
settings.WOO_CART_COMPLETE,
settings.WOO_CART_GROUP_BASIC,
settings.WOO_CART_GROUP_COMPLETE)
)
if settings.WOO_CART_ZERO:
valid_cart_numbers.append("0")
......@@ -34,6 +123,12 @@ class CartRegistrationView(RegistrationView):
if settings.WOO_CART_999:
valid_cart_numbers.append("999")
if settings.WOO_CART_5000:
valid_cart_numbers.append("5000")
if settings.WOO_CART_5999:
valid_cart_numbers.append("5999")
self.reward = kwargs.get('reward', None)
if self.reward not in valid_cart_numbers:
......@@ -47,37 +142,96 @@ class CartRegistrationView(RegistrationView):
def get_success_url(self, user):
if self.reward in ["0", "999"]:
# skip billing requirements
if self.reward in ["0", "999", "5000", "5999"]:
# skip billing page for magic cart numbers
# (but still force authentication for login purposes)
url = '{}/{}'.format(
url = '{}/{}/{}&reauth=1'.format(
settings.WOO_URL,
'wp-login.php?redirect_to=',
settings.WOO_CART_THANKS_PATH,
)
else:
# go to WooCommerce cart URL
url = '{}/{}/{}'.format(
if hasattr(user, 'chosenreward'):
user_reward = user.chosenreward.reward
else:
user_reward = 1
url = '{}/{}/{}/{}&reauth=1'.format(
settings.WOO_URL,
'wp-login.php?redirect_to=',
settings.WOO_CART_BILLING_PATH,
self.reward
user_reward
)
return url
def register(self, form):
if self.reward == 0:
# TODO: register as basic user
user = super().register(form)
elif self.reward == 999:
# TODO: register as complete user
user = super().register(form)
else:
# TODO: register as pending user
user = super().register(form)
user = super().register(form)
reward = ChosenReward(user=user, reward=self.reward)
magic_carts = ["0", "999", "5000", "5999"]
basic_carts = list(set().union(["0", "5000"],
settings.WOO_CART_BASIC,
settings.WOO_CART_GROUP_BASIC))
complete_carts = list(set().union(["999", "5999"],
settings.WOO_CART_COMPLETE,
settings.WOO_CART_GROUP_COMPLETE))
group_carts = list(set().union(["5000", "5999"],
settings.WOO_CART_GROUP_BASIC,
settings.WOO_CART_GROUP_COMPLETE))
is_magic = self.reward in magic_carts
if is_magic:
reward.is_pending = False
# List of services that need to be activated for the new user
to_activate = []
if self.reward in basic_carts:
# Register as BASIC user
user.account_type = AccountType.BASIC
if is_magic:
reward.is_pending = False
# Activate Chat and Social
to_activate.append(settings.LM_SERVICES.SOCIAL)
to_activate.append(settings.LM_SERVICES.CHAT)
for lgroup in ['chat', 'social']:
user.set_ldap_group(lgroup)
elif self.reward in complete_carts:
# Register as COMPLETE user
user.account_type = AccountType.COMPLETE
if is_magic:
reward.is_pending = False
# Activate all but Group
to_activate.append(settings.LM_SERVICES.SOCIAL)
to_activate.append(settings.LM_SERVICES.CHAT)
to_activate.append(settings.LM_SERVICES.TUNNEL)
to_activate.append(settings.LM_SERVICES.MAIL)
to_activate.append(settings.LM_SERVICES.XMPP)
for lgroup in ['chat', 'social', 'mail', 'xmpp']:
user.set_ldap_group(lgroup)
if self.reward in group_carts:
user.account_type = AccountType.GROUP
if is_magic:
reward.is_pending = False
# Activate Group
to_activate.append(settings.LM_SERVICES.GROUP)
now = timezone.now()
next_month = now + timezone.timedelta(days=30)
limits = user.limit_set.all()
for limit in limits:
if limit.service in to_activate:
limit.renewal_date = next_month
# Tunnel special case. Requires user manual activation
if limit.service == settings.LM_SERVICES.TUNNEL:
limit.is_active = False
else:
limit.is_active = True
limit.save()
user.save()
reward.save()
return user
......@@ -13,6 +13,7 @@ DEBUG_CHANGE_PASSWORD=False
DEBUG_REGISTER_STATUS = True
DEBUG_SKIP_ACTIVATION_COMMAND=True
DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION = False
DEBUG_REMOVE_CAPTCHA = False
# change to false after initial setup
ALLOWED_HOSTS=localhost
STATIC_ROOT=/var/opt/purist/middleware/static
......@@ -21,21 +22,57 @@ REG_PERSON_BASE_DN=ou=people,dc=example,dc=com
REG_PERSON_OBJECT_CLASSES=inetOrgPerson,organizationalPerson,person
REG_GROUP_BASE_DN=ou=groups,dc=example,dc=com
REG_GROUP_OBJECT_CLASSES=groupOfNames
REG_BAD_SUBSTRINGS=peanut,butter,jelly,time
AUTH_LDAP_SERVER_URI=ldap://ldap.example.com
AUTH_LDAP_START_TLS=True
AUTH_LDAP_BIND_DN=cn=admin,dc=example,dc=com
AUTH_LDAP_USER_SEARCH_BASE_DN=ou=people,dc=example,dc=com
SQLITE_DB_PATH=/var/opt/purist/middleware/db.sqlite3
STATICFILES_DIRS=/var/opt/purist/brand,/var/opt/purist/downloads
WOO_URL=https://example.com
WOO_WP_API=True
WOO_VERSION=wc/v1
WOO_QUERY_STRING_AUTH = True
WOOSUB1_PRODUCT_LIST = 123,124
WOO_CART_PATH=cart
WOO_CART_BASIC = 1
WOO_CART_COMPLETE = 2
WOO_CART_GROUP_BASIC = 3
WOO_CART_GROUP_COMPLETE = 4
WOO_CART_ZERO = True
WOO_CART_999 = True
WOO_CART_5000 = True
WOO_CART_5999 = True
WOO_CART_BILLING_PATH = add_to_cart
WOO_CART_THANKS_PATH = get-started
WOO_API_TIMEOUT = 30
WOO_PARENT_PROD_ID = 34
WOO_UPGRADE_PAY_URL = http://wp.example.com/checkout/order-pay/{}/?pay_for_order=true&key={}
WOO_BUNDLES_INFO_FILE = /etc/opt/purist/middleware/upgrades_downgrades.yml
OVPN_HOSTNAME=ssh.example.com
OVPN_PORT=22
OVPN_USERNAME=username
OVPN_FILEPATH="/path/to/{IDENTITY}/{IDENTITY}.ovpn"
TUNNEL_HOST=https://example.com
PASSWORD_RESET_TOKEN_EXPIRES=1800
# Email configuration example to use Mailtrap for development (https://source.puri.sm/snippets/56)
# EMAIL_HOST = '127.0.0.1'
# EMAIL_HOST_USER = ''
# EMAIL_PORT = 1025
# EMAIL_USE_TLS = False
# Email configuration sample for production/staging
DEFAULT_FROM_EMAIL = admin@example.com
SERVER_EMAIL = noreply@example.com
EMAIL_HOST = smtp.example.com
EMAIL_HOST_USER = jhon.doe@example.com
EMAIL_PORT = 465
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
DEFAULT_FROM_EMAIL = "Do not reply <noreply@example.com>"
TUNNEL_HOST=https://example.com
\ No newline at end of file
# This is the link to the subscribe page, used in different parts of the middleware
SUBSCRIBE_HREF = /#bundle
-
name: BASIC
services:
- Chat
- Social
ids:
- 62889
- 54390
-
name: COMPLETE
services:
- Chat
- Social
- Xmpp
- Mail
- Tunnel
ids:
- 62123
- 56584
- 64655
-
name: GROUP_COMPLETE
services:
- Chat
- Social
- Xmpp
- Mail
- Tunnel
- Group
ids:
- 62124
- 56586
- 64657
-
name: GROUP_BASIC
services:
- Chat
- Social
ids:
- 62887
- 54393
......@@ -14,4 +14,13 @@ TUNNEL_SECRET = secret
# print(Fernet.generate_key().decode())'
# You should get something like this:
# jBv_8z3mq0jurqeB5MFYav1n6MTyWd4bOXJ8tzy9ywU=
TUNNEL_KEY = generate_your_own_key!
\ No newline at end of file
TUNNEL_KEY = generate_your_own_key!
EMAIL_HOST_PASSWORD = v3r153cR37
# PostgreSQL config
PSQL_DB_NAME = django_middleware
PSQL_USER_NAME = django
PSQL_USER_PASSWORD = supersecret
PSQL_HOST = psql.example.com
PSQL_PORT = 5432
\ No newline at end of file
12345:
title: Example Basic Bundle (monthly)
short_title: Basic (monthly)
type: BASIC
period: monthly
domain: example.com
upgrades:
- 12346
- 12347
- 12348
- 12349
downgrades:
12350:
title: Example Basic Bundle (annual)
short_title: Basic (annual)
type: BASIC
period: annual
domain: example.com
upgrades:
- 12351
- 12347
- 12348
- 12349
downgrades:
12351:
title: Example Complete Bundle (monthly)
short_title: Complete (monthly)
type: COMPLETE
period: monthly
domain: example.com
upgrades:
- 12347
- 12348
- 12349
downgrades:
- 12345
- 12350
12347:
title: Example Complete Bundle (annual)
short_title: Complete (annual)
type: COMPLETE
period: annual
domain: example.com
upgrades:
- 12348
- 12349
downgrades:
- 12345
- 12350
12348:
title: Example Family Pack (monthly)
short_title: Family pack (monthly)
type: GROUP
period: monthly
domain: example.com
upgrades:
downgrades:
- 12345
- 12350
- 12347
- 12351
12349:
title: Example Family Pack (annual)
short_title: Family pack (annual)
type: GROUP
period: annual
domain: example.com
upgrades:
downgrades:
- 12345
- 12350
- 12347
- 12351
; stored as /etc/supervisor/conf.d/purist_middleware_monitor.conf
; Copyright 2017-2018 Purism SPC
; Copyright 2017-2019 Purism SPC
; SPDX-License-Identifier: AGPL-3.0-or-later
; Adapted from extra/supervisord/celerybeat.conf in Celery
......
......@@ -57,7 +57,7 @@ master_doc = 'index'
# General information about the project.
project = 'Keel (LDH middleware)'
copyright = '2017-2018 Purism SPC'
copyright = '2017-2019 Purism SPC'
author = 'Purism SPC'
# The version info for the project you're documenting, acts as replacement for
......
from django.contrib import admin
from .models import Invitation
class InvitationAdmin(admin.ModelAdmin):
list_display = ['owner', 'guest', 'msg', 'msg_hash',
'created_date', 'consumed']
admin.site.register(Invitation, InvitationAdmin)
from django.apps import AppConfig
class InvitationConfig(AppConfig):
name = 'invitation'
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-05 22:49
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Invitation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('msg', models.CharField(max_length=250)),
('msg_hash', models.CharField(max_length=250)),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('consumed', models.BooleanField(default=False)),
('guest', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='guests', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
from django.db import models
from django.conf import settings
from django.utils import timezone
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.exceptions import InvalidSignature
import base64
import binascii
from django.core.exceptions import ValidationError
class InvitationManager(models.Manager):
def get_invitation_by_hash(self, msg_hash):
invitations = self.filter(msg_hash=msg_hash)
if len(invitations) > 0:
return invitations.first()
return None
class Invitation(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
msg = models.CharField(max_length=250)
msg_hash = models.CharField(max_length=250)
created_date = models.DateTimeField(default=timezone.now)
consumed = models.BooleanField(default=False)
guest = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='guests',
null=True)
objects = InvitationManager()
def save(self, *args, **kwargs):
if self.msg_hash == '':
raise ValidationError('msg_hash cannot be empty.')
super(Invitation, self).save(*args, **kwargs)
@classmethod
def _create_HMAC_SHA256_hash(cls, message):
key = settings.SECRET_KEY.encode()
h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend())
h.update(message.encode())
urlsafe_hash = base64.urlsafe_b64encode(h.finalize())
return urlsafe_hash.decode()
@classmethod
def _validate_HMAC_SHA256_hash(cls, urlsafe_hash, message):
key = settings.SECRET_KEY.encode()
h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend())
h.update(message.encode())
try:
h.verify(base64.urlsafe_b64decode(urlsafe_hash.encode()))
except (InvalidSignature, binascii.Error):
return False
else:
return True
@classmethod
def create(cls, user):
inv = cls(owner=user)
message = '{user}::{time}'.format(
user=user.username,
time=timezone.now()
)
inv.msg_hash = cls._create_HMAC_SHA256_hash(message)
inv.msg = message
return inv
def validate(self, urlsafe_hash):
return self._validate_HMAC_SHA256_hash(urlsafe_hash, self.msg)
def expired(self):
now = timezone.now()
return (now - self.created_date).total_seconds() > 60 * 60 * 24
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans 'Invitation' %}{% endblock %}
{% block extra_css%}{% include "purist/spinner.html" %}{% endblock %}
{% block header %}{% trans 'Invitation' %}{% endblock %}
{% block byline %}{% trans 'Please fill in your invitation details' %}{% endblock %}
{% block login_status %}
{% if DEBUG_REGISTER_STATUS %}
{{ block.super }}
{% endif %}
{% endblock %}
{% block content %}
<form id="register_invitee" method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans 'Register >>' %}" />
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endblock %}
{% block extra_js %}
<script>
const frm = document.getElementById('register_invitee'),
spin = document.getElementById('spinner-wrap');
frm.addEventListener('submit', enable_spinner);
function enable_spinner() {
spin.setAttribute('class', 'spinner-wrapper');
}
</script>
{% endblock %}
from django.test import TestCase
# Create your tests here.
from django.conf import settings
from django.contrib.auth import logout
from django.http import Http404
from django.urls import reverse
from invitation.models import Invitation
from registration.backends.simple.views import RegistrationView
from purist.models import AccountType
from cart.views import CartRegistrationFormWithCaptcha as \
InvitationRegistrationForm
from limitmonitor.task_resources.subscription import \
create_invitee_subscription
from limitmonitor.common import forced_update
class InvitationRegistrationView(RegistrationView):
form_class = InvitationRegistrationForm
template_name = 'invitation/registration_form.html'
msg_hash = None
invitation = None
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['DEBUG_REGISTER_STATUS'] = settings.DEBUG_REGISTER_STATUS
return context
def get(self, *args, **kwargs):
logout(self.request)
self.msg_hash = kwargs.get('msg_hash', None)
self.invitation = Invitation.objects.get_invitation_by_hash(
self.msg_hash)
inv = self.invitation
if inv is None or not inv.validate(self.msg_hash):
raise Http404('Invalid invitation')
elif inv.consumed:
raise Http404('Invitation already used')
elif inv.expired():
raise Http404('Invitation expired')
elif len(inv.owner.invitation_set.filter(consumed=True)) >= 4:
raise Http404('All memberships have been taken')
return super().get(*args, **kwargs)
def post(self, *args, **kwargs):
self.msg_hash = kwargs.get('msg_hash', None)
self.invitation = Invitation.objects.get_invitation_by_hash(
self.msg_hash)
return super().post(*args, **kwargs)
def get_success_url(self, user):
return reverse('profile')
def register(self, form):
# Create new user and set AccountType to INVITED
user = super().register(form)
user.account_type = AccountType.INVITED
user.save()
# set the invitation to consumed and link the guest user
self.invitation.consumed = True
self.invitation.guest = user
self.invitation.save()
# Create an Invitee subscritpion for the user
sub = create_invitee_subscription(user)
if sub:
forced_update(user)
return user
......@@ -11,7 +11,7 @@ User = get_user_model()
username_label = _('%(service)s address' % {'service': settings.SITE_TITLE})
class Authenticationform(BaseAuthenticationForm):
class LdhAuthenticationForm(BaseAuthenticationForm):
username = UsernameField(
label=username_label,
max_length=254,
......@@ -40,10 +40,7 @@ class RegistrationForm(BaseRegistrationForm):
label=_("Passphrase confirmation"),
widget=forms.PasswordInput,
strip=False,
help_text=_("Enter the same Passphrase as before, for verification."),
)
captcha = CaptchaField(
label=_('Are you a robot?')
help_text=_("Enter the same passphrase as before, for verification."),
)
def __init__(self, *args, **kwargs):
......
......@@ -31,10 +31,10 @@ class LdapGroup(ldapdb.models.Model):
members = ListField(db_column='member')
def __str__(self):
return self.name
return self.cn
def __unicode__(self):
return self.name
return self.cn
class LdapPerson(ldapdb.models.Model):
......
......@@ -2,7 +2,7 @@
{% load i18n %} {% load static %}
<!--
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......@@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0
<link rel="icon" sizes="960x960" href="{% static 'favicon.png' %}"/>
<meta name="application-name" content="Purist"/>
<meta charset="UTF-8"/>
{% block extra_css%}{% endblock %}
</head>
<body>
......@@ -53,9 +54,7 @@ SPDX-License-Identifier: AGPL-3.0
<main>
<div class="form">
{% block content %}{% endblock %}
</div>
</main>
......@@ -72,6 +71,6 @@ SPDX-License-Identifier: AGPL-3.0
</div>
</footer>
{% block extra_js%}{% endblock %}
</body>
</html>
......@@ -4,11 +4,18 @@
{% block header %}{% trans 'Log in' %}{% endblock %}
{% block byline %}{% trans 'Please fill in your credentials' %}{% endblock %}
{% block content %}
{% block extra_css%}
<style>
.login p {
margin-left: 40px;
}
</style>
{% endblock %}
{% block content %}
<!-- Main Content -->
<section class="row">
<section>
<section class="login">
<form method="post" action=".">
{% csrf_token %} {{ form|crispy}}
......@@ -17,7 +24,8 @@
<input type="hidden" name="next" value="{{ next }}"/>
</div>
</form>
<p>new user? <a href="{{ site_settings.SUBSCRIBE_HREF }}">register now!</a></p>
<p><a href="{% url 'password_reset_recover' %}">forgot your password?</a></p>
</section>
</section>
{% endblock %}
......@@ -7,5 +7,12 @@
{% block byline %}{% trans 'You have been logged out' %}{% endblock %}
{% block content %}
{% if messages %}
<div style="padding: 20px;">
{% for message in messages %}
<p>{{ message }}
{% endfor %}
</div>
{% endif %}
{% endblock %}
from django.conf import settings
from django.contrib.auth.views import LoginView
from .forms import Authenticationform
from .forms import LdhAuthenticationForm
class LdhLoginView(LoginView):
form_class = Authenticationform
form_class = LdhAuthenticationForm
from django.contrib import admin
from .models import Credit, ExternalBundle, ExternalCredit, Limit
from .models import ExternalBundle, ExternalCredit, Limit
#
# Declare admin models
#
class CreditAdmin(admin.ModelAdmin):
list_display = ['id', 'limit', 'external', 'time_credit', 'volume_credit']
class ExternalBundleAdmin(admin.ModelAdmin):
list_display = ['id', 'parser', 'external_key', 'service', 'time_credit', 'volume_credit']
......@@ -27,7 +24,6 @@ class LimitAdmin(admin.ModelAdmin):
# Register admin models
#
admin.site.register(Credit, CreditAdmin)
admin.site.register(ExternalBundle, ExternalBundleAdmin)
admin.site.register(ExternalCredit, ExternalCreditAdmin)
admin.site.register(Limit, LimitAdmin)
This diff is collapsed.
......@@ -2,7 +2,7 @@
<!--
Keel (LDH middleware)
Copyright 2017-2018 Purism SPC
Copyright 2017-2019 Purism SPC
https://source.puri.sm/liberty/ldh_middleware
SPDX-License-Identifier: AGPL-3.0
-->
......@@ -26,7 +26,22 @@ SPDX-License-Identifier: AGPL-3.0
<a href="{% url 'home' %}"><img class="logo" src="{% static 'logo.png' %}" alt="{{ site_title }}"/></a>
<div id="title_text">
<h1>{% trans "User profile" %}</h1>
{% if haschosenreward and request.user.chosenreward.is_pending %}
<div class="notice">
<p>
Your registration is pending. If you completed the billing process, please wait two minutes before refreshing this page.
</p>
{% if not haspaid %}
<p>If you have <strong><i>not</i></strong> completed the billing process, <a href="{{complete_signup_url}}">please do so now.</a>
</p>
<p>
Pending registrations are removed after 24 hours.
</p>
{% endif %}
</div>
{% else %}
<p>{% trans "Service credit and profile management" %}</p>
{% endif %}
</div>
</div>
......@@ -49,11 +64,11 @@ SPDX-License-Identifier: AGPL-3.0
-->
{% endif %}
</div>
</header>
<hr/>
{% if (haschosenreward and not request.user.chosenreward.is_pending) or not haschosenreward %}
<main class="col-wrapper form">
<article class="col-1">
......@@ -73,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0
<tbody>
{% for limit in limits %}
{% if li