Commit a2340d89 authored by Noe Nieto's avatar Noe Nieto 💬

Merge

parents ee87d597 9c6e8e75
Pipeline #5664 canceled with stage
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)
......@@ -59,7 +59,11 @@ class CartRegistrationView(RegistrationView):
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)
)
if settings.WOO_CART_ZERO:
valid_cart_numbers.append("0")
......@@ -112,6 +116,8 @@ class CartRegistrationView(RegistrationView):
elif self.reward == 999 or self.reward in settings.WOO_CART_COMPLETE:
# Register as COMPLETE user
user.account_type = AccountType.COMPLETE
elif self.reward == 3 or self.reward in settings.WOO_CART_GROUP:
user.account_type = AccountType.GROUP
else:
# Register as UNDEFINED user
user.account_type = AccountType.UNDEFINED
......
......@@ -40,5 +40,5 @@ OVPN_USERNAME=username
OVPN_FILEPATH="/path/to/{IDENTITY}/{IDENTITY}.ovpn"
TUNNEL_HOST=https://example.com
PASSWORD_RESET_TOKEN_EXPIRES=1800
WOO_CART_BASIC
WOO_CART_COMPLETE
\ No newline at end of file
WOO_CART_GROUP = 1,2,4
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 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 method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans 'Submit' %}" />
</form>
{% 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 django.utils.translation import ugettext_lazy as _
from captcha.fields import CaptchaField
from invitation.models import Invitation
from ldapregister.forms import RegistrationForm
from registration.backends.simple.views import RegistrationView
from purist.models import AccountType
class InvitationRegistrationForm(RegistrationForm):
captcha = CaptchaField(
label=_('Please solve this sum'),
help_text=_('Prove you are not a robot. '
'Solve this addition, subtraction or multiplication '
'problem.')
)
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()
# Active same limists/services as the owner
user_limits = user.limit_set.all()
owner_limits = self.invitation.owner.limit_set.all()
for o_limit in owner_limits:
o_service = o_limit.service
if o_service != settings.LM_SERVICES.GROUP:
for u_limit in user_limits:
if u_limit.service == o_service:
u_limit.renewal_date = o_limit.renewal_date
u_limit.expiry_date = o_limit.expiry_date
u_limit.volume_total = o_limit.volume_total
u_limit.time_total = o_limit.time_total
u_limit.is_active = o_limit.is_active
u_limit.created_date = o_limit.created_date
u_limit.updated_date = o_limit.updated_date
# Generate tunnel creds for invited user if
# owner has tunnel service
if (
o_service == settings.LM_SERVICES.TUNNEL and
o_limit.remaining_use_time().total_seconds() > 0
):
user.generate_tunnel_account()
u_limit.is_active = True
u_limit.save()
return user
......@@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0
<tbody>
{% for limit in limits %}
{% if limit.service_label() != 'Group' %}
<tr>
<th class="row_header">{{ limit.service_label() }}</th>
</th>
......@@ -81,8 +81,8 @@ SPDX-License-Identifier: AGPL-3.0
<td title="{{ limit.endpoint_full_label() }}">{{ limit.endpoint_short_label() }}</td>
<td>{{ limit.credit_label() }}</td>
<td>
<!-- We will have to change the action url per type of service -->
<form method="post" action="{% url 'toggle_tunnel' %}">{% csrf_token %}
<form method="post" action="{{ action_function[limit.service_label().upper()] }}">
{% csrf_token %}
{% if limit.is_active and limit.service_activable_by_user() %}
<input type="submit" value="Deactivate"/>
{% elif not limit.is_active and limit.service_activable_by_user() %}
......@@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0
</form>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
......@@ -112,6 +112,43 @@ SPDX-License-Identifier: AGPL-3.0
{% endif %}
</ul>
{% if has_limit["GROUP"]%}
<h2>{% trans "Group members" %}</h2>
<ul>
{% if invitations['no_members'] %}
<li>{% trans "None" %}</li>
{% else %}
{% for invitation in invitations['list'] %}
{% if invitation['consumed'] %}
<li>{{ invitation.guest }}</li>
{% endif %}
{% endfor %}
{% endif%}
</ul>
<h2>{% trans "Group invites" %}</h2>
<ul>
{% if not invitations['links_not_used'] %}
<li>{% trans "None left" %}</li>
{% else %}
{% for invitation in invitations['list'] %}
{% if not invitation['expired'] and not invitation['consumed'] %}
<li><a href="{{ invitation["link"] }}">{{ invitation["link"] }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
</ul>
<form method="post" action="{% url 'new_invitation' %}">
{% csrf_token %}
{% if invitations["full"] %}
<input type="submit" value="New invitation" disabled/>
{% else %}
<input type="submit" value="New invitation"/>
{% endif %}
</form>
{% endif %}
{% if DEBUG_CHANGE_PASSWORD %}
<h2>{% trans "Profile management" %}</h2>
......
import datetime
from .common import *
......@@ -28,7 +27,7 @@ def parse_woosub1(json_entry):
# bundle_key value is different depending on the subscription
# having variations or not
if line_item.get('variation_id') is not None:
if line_item.get('variation_id') != 0:
bundle_key = line_item.get('variation_id')
else:
bundle_key = line_item.get('product_id')
......@@ -62,7 +61,6 @@ def monitor_woosub1_new_subscriptions():
'orderby=date&'
'order=desc&'
'status=active').json()
# parse recent subscriptions and store results
for json_entry in latest_subscription_json:
try:
......
......@@ -5,6 +5,8 @@ from django.shortcuts import render
from django.urls import reverse
from .models import Limit
from invitation.models import Invitation
from purist.models import AccountType
@login_required
......@@ -14,16 +16,42 @@ def userlimit(request):
# get flags for each limit
has_limit = {}
action_function = {}
none_limit = True
for limit in limits:
label = limit.service_label().upper()
has_limit[label] = limit.is_active
# Default action for services
action_function[label] = '.'
if limit.is_active:
none_limit = False
has_limit["NONE"] = none_limit # true if no limits are active
action_function["TUNNEL"] = reverse("toggle_tunnel")
# Prepare context variables for Group service
invitations = {'list': []}
inv_all = request.user.invitation_set.all()
invitations['full'] = len(inv_all) >= 4
invitations['no_members'] = True
invitations['links_not_used'] = False
for inv in inv_all:
inv_dict = {}
inv_dict['link'] = request.build_absolute_uri(
reverse('register_invitation', kwargs={'msg_hash': inv.msg_hash})
)
inv_dict['consumed'] = inv.consumed
if inv.guest is not None:
inv_dict['guest'] = inv.guest.username
invitations['no_members'] = False
else:
inv_dict['guest'] = '---'
inv_dict['expired'] = inv.expired()
if not inv.expired() and not inv.consumed:
invitations['links_not_used'] = True
invitations['list'].append(inv_dict)
render_data = {
"DEBUG_CHANGE_PASSWORD": settings.DEBUG_CHANGE_PASSWORD,
"username": username,
......@@ -34,6 +62,8 @@ def userlimit(request):
"limits": limits,
"has_limit": has_limit,
"link_profile_ordered_dict": settings.LINK_PROFILE_ORDERED_DICT,
"action_function": action_function,
"invitations": invitations,
}
return render(request, 'limitmonitor/userlimit.html', render_data)
......@@ -48,6 +78,7 @@ def ovpn_userfile(request):
response['Content-Disposition'] = 'attachment; filename="purist.ovpn"'
return response
@login_required
def toggle_tunnel(request):
tunnel_limit = request.user.limit_set.filter(
......@@ -58,3 +89,13 @@ def toggle_tunnel(request):
tunnel_limit.is_active = not tunnel_limit.is_active
tunnel_limit.save()
return HttpResponseRedirect(reverse('profile'))
@login_required
def new_invitation(request):
if request.user.account_type == AccountType.GROUP:
invs_count = len(request.user.invitation_set.all())
if invs_count < 4:
new_inv = Invitation.create(request.user)
new_inv.save()
return HttpResponseRedirect(reverse('profile'))
......@@ -36,6 +36,7 @@ DEBUG_CHANGE_PASSWORD = config("DEBUG_CHANGE_PASSWORD", cast=bool)
DEBUG_SKIP_ACTIVATION_COMMAND = config("DEBUG_SKIP_ACTIVATION_COMMAND", cast=bool)
DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION = config("DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION", cast=bool)
DEBUG_REGISTER_STATUS = config("DEBUG_REGISTER_STATUS", cast=bool)
DEBUG_LOCAL_MAIL = config("DEBUG_LOCAL_MAIL", cast=bool)
# Required if DEBUG is False
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
......@@ -55,7 +56,8 @@ INSTALLED_APPS += ["crispy_forms",
"cart",
"registration",
"rest_framework",
"password_reset"]
"password_reset",
"invitation"]
#
# AGPL APPLICATION
......@@ -165,6 +167,7 @@ WOO_CONSUMER_SECRET = secret_config("WOO_CONSUMER_SECRET")
WOO_CART_BASIC = config("WOO_CART_BASIC", cast=Csv(str))
WOO_CART_COMPLETE = config("WOO_CART_COMPLETE", cast=Csv(str))
WOO_CART_GROUP = config("WOO_CART_GROUP", cast=Csv(str))
WOO_CART_ZERO = config("WOO_CART_ZERO", cast=bool)
WOO_CART_999 = config("WOO_CART_999", cast=bool)
WOO_CART_BILLING_PATH = config("WOO_CART_BILLING_PATH")
......@@ -249,7 +252,7 @@ REST_FRAMEWORK = {
#
# django password recover: 30 min
#
PASSWORD_RESET_TOKEN_EXPIRES = secret_config('PASSWORD_RESET_TOKEN_EXPIRES')
PASSWORD_RESET_TOKEN_EXPIRES = secret_config('PASSWORD_RESET_TOKEN_EXPIRES', 1800)
if DEBUG_LOCAL_MAIL:
......
......@@ -25,6 +25,8 @@ from ldapregister.views import LdhLoginView
from cart.views import CartRegistrationView
from purist.views import Recovery
from invitation.views import InvitationRegistrationView
#
# Set admin titles for this site
#
......@@ -42,8 +44,10 @@ urlpatterns = [
url(r'^accounts/$', RedirectView.as_view(url='/')),
url(r'^accounts/profile/$', limitmonitor.views.userlimit, name='profile'),
url(r'^accounts/profile/purist.ovpn', limitmonitor.views.ovpn_userfile, name='ovpn_userfile'),
url(r'^accounts/profile/toggle_tunnel',
limitmonitor.views.toggle_tunnel, name='toggle_tunnel'),
url(r'^accounts/profile/toggle_tunnel',
limitmonitor.views.toggle_tunnel, name='toggle_tunnel'),
url(r'^accounts/profile/new_invitation',
limitmonitor.views.new_invitation, name='new_invitation'),
# url(r'^accounts/register/$', RegistrationView.as_view(form_class=RegistrationForm), name='registration_register'),
url(r'^accounts/login/$', LdhLoginView.as_view(), name='auth_login'),
url(r'^accounts/recover/$', Recovery.as_view(), name='password_reset_recover'),
......@@ -53,6 +57,9 @@ urlpatterns = [
url(r'^jslicense/$', purist.views.jslicense, name='jslicense'),
url(r'^captcha/', include('captcha.urls')),
url(r'^cart/(?P<reward>\d+)/$', CartRegistrationView.as_view(), name="register_reward"),
url(r'^invitation/(?P<msg_hash>.+)/$',
InvitationRegistrationView.as_view(),
name="register_invitation"),
url(r'^api/v1/user/tunnel_account',
purist.views.UserDetail.as_view(), name='user_details'),
url(r'^public/(?P<target>.*)',
......
......@@ -31,6 +31,8 @@ class ServicesContainer(Container):
MAIL = 3
SOCIAL = 4
XMPP = 5
GROUP = 6
# MAPPING
MAP = OrderedDict([
......@@ -40,6 +42,7 @@ class ServicesContainer(Container):
(MAIL, _("Mail")),
(SOCIAL, _("Social")),
(XMPP, _("XMPP")),
(GROUP, _("Group")),
])
......
......@@ -17,6 +17,7 @@ from limitmonitor.tunnel import TunnelManager
from cryptography.fernet import Fernet
from choicesenum import ChoicesEnum
from choicesenum.django.fields import EnumIntegerField
from invitation.models import Invitation
from woocommerce import API as WOO_API
......@@ -27,6 +28,8 @@ class AccountType(ChoicesEnum):
UNDEFINED = 0
BASIC = 1
COMPLETE = 2
INVITED = 3
GROUP = 4
def get_woo_connection():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment