redeem access permission tokens
This commit is contained in:
parent
003bfbe389
commit
101a4c6bf2
12 changed files with 133 additions and 19 deletions
|
@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ungettext_lazy
|
from django.utils.translation import ungettext_lazy
|
||||||
|
|
||||||
from c3nav.control.models import UserPermissions
|
from c3nav.control.models import UserPermissions
|
||||||
from c3nav.mapdata.models.access import AccessPermissionToken, AccessRestriction
|
from c3nav.mapdata.models.access import AccessPermissionToken, AccessPermissionTokenItem, AccessRestriction
|
||||||
|
|
||||||
|
|
||||||
class UserPermissionsForm(ModelForm):
|
class UserPermissionsForm(ModelForm):
|
||||||
|
@ -99,11 +99,12 @@ class AccessPermissionForm(Form):
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
restrictions = []
|
restrictions = []
|
||||||
for restriction in self.cleaned_data['access_restrictions']:
|
for restriction in self.cleaned_data['access_restrictions']:
|
||||||
expires = self.cleaned_data['expires']
|
expire_date = self.cleaned_data['expires']
|
||||||
author_expires = self.author_access_permissions.get(restriction.pk)
|
author_expire_date = self.author_access_permissions.get(restriction.pk)
|
||||||
if author_expires is not None:
|
if author_expire_date is not None:
|
||||||
expires = author_expires if expires is None else min(expires, author_expires)
|
expire_date = author_expire_date if expire_date is None else min(expire_date, author_expire_date)
|
||||||
restrictions.append((restriction.pk, expires))
|
restrictions.append(AccessPermissionTokenItem(pk=restriction.pk, expire_date=expire_date,
|
||||||
|
title=restriction.title))
|
||||||
return AccessPermissionToken(author=self.author,
|
return AccessPermissionToken(author=self.author,
|
||||||
can_grant=self.cleaned_data.get('can_grant', '0') == '1',
|
can_grant=self.cleaned_data.get('can_grant', '0') == '1',
|
||||||
restrictions=tuple(restrictions))
|
restrictions=tuple(restrictions))
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
{% trans 'Scan this QR code to get access permissions:' %}
|
{% trans 'Scan this QR code to get access permissions:' %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<img src="/qr/access/{{ token }}">
|
<img src="{{ url_qr }}">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ absolute_url }}
|
{{ url_absolute }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'control.access' %}">« {% trans 'back' %}</a>
|
<a href="{% url 'control.access' %}">« {% trans 'back' %}</a>
|
||||||
|
|
|
@ -6,6 +6,6 @@ urlpatterns = [
|
||||||
url(r'^users/$', user_list, name='control.users'),
|
url(r'^users/$', user_list, name='control.users'),
|
||||||
url(r'^users/(?P<user>\d+)/$', user_detail, name='control.users.detail'),
|
url(r'^users/(?P<user>\d+)/$', user_detail, name='control.users.detail'),
|
||||||
url(r'^access/$', grant_access, name='control.access'),
|
url(r'^access/$', grant_access, name='control.access'),
|
||||||
url(r'^access/qr/(?P<token>[^/]+)', grant_access_qr, name='control.access.qr'),
|
url(r'^access/(?P<token>[^/]+)$', grant_access_qr, name='control.access.qr'),
|
||||||
url(r'^$', main_index, name='control.index'),
|
url(r'^$', main_index, name='control.index'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -155,12 +155,14 @@ def grant_access_qr(request, token):
|
||||||
messages.error(request, _('You can only display your most recently created token.'))
|
messages.error(request, _('You can only display your most recently created token.'))
|
||||||
|
|
||||||
if token is None:
|
if token is None:
|
||||||
redirect('control.access')
|
return redirect('control.access')
|
||||||
|
|
||||||
token.bump()
|
token.bump()
|
||||||
token.save()
|
token.save()
|
||||||
|
|
||||||
|
url = reverse('site.access.redeem', kwargs={'token': str(token.id)})
|
||||||
return render(request, 'control/access_qr.html', {
|
return render(request, 'control/access_qr.html', {
|
||||||
'token': token.id,
|
'url': url,
|
||||||
'absolute_url': request.build_absolute_uri('/access/qr/%s' % token.id)
|
'url_qr': reverse('site.qr', kwargs={'path': url}),
|
||||||
|
'url_absolute': request.build_absolute_uri(url),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import pickle
|
import pickle
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import namedtuple
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -35,6 +37,9 @@ def default_valid_until():
|
||||||
return timezone.now()+timedelta(seconds=20)
|
return timezone.now()+timedelta(seconds=20)
|
||||||
|
|
||||||
|
|
||||||
|
AccessPermissionTokenItem = namedtuple('AccessPermissionTokenItem', ('pk', 'expire_date', 'title'))
|
||||||
|
|
||||||
|
|
||||||
class AccessPermissionToken(models.Model):
|
class AccessPermissionToken(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||||
|
@ -52,11 +57,11 @@ class AccessPermissionToken(models.Model):
|
||||||
data = models.BinaryField()
|
data = models.BinaryField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def restrictions(self):
|
def restrictions(self) -> Iterable[AccessPermissionTokenItem]:
|
||||||
return pickle.loads(self.data)
|
return pickle.loads(self.data)
|
||||||
|
|
||||||
@restrictions.setter
|
@restrictions.setter
|
||||||
def restrictions(self, value):
|
def restrictions(self, value: Iterable[AccessPermissionTokenItem]):
|
||||||
self.data = pickle.dumps(value)
|
self.data = pickle.dumps(value)
|
||||||
|
|
||||||
def redeem(self, user=None):
|
def redeem(self, user=None):
|
||||||
|
@ -68,13 +73,13 @@ class AccessPermissionToken(models.Model):
|
||||||
|
|
||||||
self.redeemed = True
|
self.redeemed = True
|
||||||
if user:
|
if user:
|
||||||
for pk, expire_date in self.restrictions:
|
for restriction in self.restrictions:
|
||||||
obj, created = AccessPermission.objects.get_or_create(
|
obj, created = AccessPermission.objects.get_or_create(
|
||||||
user=user,
|
user=user,
|
||||||
access_restriction_id=pk
|
access_restriction_id=restriction.pk
|
||||||
)
|
)
|
||||||
obj.author_id = self.author_id
|
obj.author_id = self.author_id
|
||||||
obj.expire_date = expire_date
|
obj.expire_date = restriction.expire_date
|
||||||
obj.can_grant = self.can_grant
|
obj.can_grant = self.can_grant
|
||||||
obj.save()
|
obj.save()
|
||||||
self.redeemed_by = user
|
self.redeemed_by = user
|
||||||
|
|
|
@ -686,6 +686,22 @@ ul.messages li.alert-danger {
|
||||||
background-color:#FFEEEE;
|
background-color:#FFEEEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#messages {
|
||||||
|
z-index: 5;
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
#messages ul.messages {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
#messages ul.messages .close {
|
||||||
|
float: right;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.search-form input {
|
.search-form input {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -712,6 +728,14 @@ main.control form tr > * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main .narrow {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
main .narrow p, main .narrow form, main .narrow button {
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
.user-permissions-form label {
|
.user-permissions-form label {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
@ -41,6 +41,15 @@ c3nav = {
|
||||||
}
|
}
|
||||||
c3nav.continue_init();
|
c3nav.continue_init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#messages').find('ul.messages li').each(function() {
|
||||||
|
$(this).prepend(
|
||||||
|
$('<a href="#" class="close"><i class="material-icons">close</i></a>').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).parent().remove();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
continue_init: function() {
|
continue_init: function() {
|
||||||
c3nav.init_map();
|
c3nav.init_map();
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="account">
|
<main class="account">
|
||||||
|
<div class="narrow">
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
|
{% include 'site/fragment_messages.html' %}
|
||||||
|
|
||||||
{% if back_url %}
|
{% if back_url %}
|
||||||
<p><a href="{{ back_url }}">« {% trans 'back' %}</a></p>
|
<p><a href="{{ back_url }}">« {% trans 'back' %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -17,5 +20,6 @@
|
||||||
<a href="{{ bottom_link_url }}?{{ request.GET.urlencode }}">{{ bottom_link_text }}</a>
|
<a href="{{ bottom_link_url }}?{{ request.GET.urlencode }}">{{ bottom_link_text }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
24
src/c3nav/site/templates/site/confirm.html
Normal file
24
src/c3nav/site/templates/site/confirm.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'site/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<div class="narrow">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% include 'site/fragment_messages.html' %}
|
||||||
|
|
||||||
|
{% for text in texts %}
|
||||||
|
<p>{{ text }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% csrf_token %}
|
||||||
|
<button>{% if button_text %}{{ button_text }}{% else %}{{ title }}{% endif %}</button>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'site.index' %}">« {% trans 'back' %}</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %}>
|
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %}>
|
||||||
|
<section id="messages">{% include 'site/fragment_messages.html' %}</section>
|
||||||
<section id="attributions">
|
<section id="attributions">
|
||||||
<a href="{% url 'editor.index' %}" target="_blank">{% trans 'Editor' %}</a> //
|
<a href="{% url 'editor.index' %}" target="_blank">{% trans 'Editor' %}</a> //
|
||||||
<a href="/api/" target="_blank">{% trans 'API' %}</a> //
|
<a href="/api/" target="_blank">{% trans 'API' %}</a> //
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from c3nav.site.views import (account_view, change_password_view, login_view, logout_view, map_index, qr_code,
|
from c3nav.site.views import (access_redeem_view, account_view, change_password_view, login_view, logout_view,
|
||||||
register_view)
|
map_index, qr_code, register_view)
|
||||||
|
|
||||||
slug = r'(?P<slug>[a-z0-9-_.:]+)'
|
slug = r'(?P<slug>[a-z0-9-_.:]+)'
|
||||||
slug2 = r'(?P<slug2>[a-z0-9-_.:]+)'
|
slug2 = r'(?P<slug2>[a-z0-9-_.:]+)'
|
||||||
|
@ -21,4 +21,5 @@ urlpatterns = [
|
||||||
url(r'^register$', register_view, name='site.register'),
|
url(r'^register$', register_view, name='site.register'),
|
||||||
url(r'^account/$', account_view, name='site.account'),
|
url(r'^account/$', account_view, name='site.account'),
|
||||||
url(r'^account/change_password$', change_password_view, name='site.account.change_password'),
|
url(r'^account/change_password$', change_password_view, name='site.account.change_password'),
|
||||||
|
url(r'^access/(?P<token>[^/]+)$', access_redeem_view, name='site.access.redeem'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,16 +7,21 @@ from django.contrib import messages
|
||||||
from django.contrib.auth import login, logout
|
from django.contrib.auth import login, logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
|
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ungettext_lazy
|
||||||
from django.views.decorators.cache import cache_control, never_cache
|
from django.views.decorators.cache import cache_control, never_cache
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
from django.views.decorators.http import etag
|
from django.views.decorators.http import etag
|
||||||
|
|
||||||
from c3nav.mapdata.models import Location, Source
|
from c3nav.mapdata.models import Location, Source
|
||||||
|
from c3nav.mapdata.models.access import AccessPermissionToken
|
||||||
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
|
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
|
||||||
from c3nav.mapdata.utils.locations import get_location_by_slug_for_request, levels_by_short_label_for_request
|
from c3nav.mapdata.utils.locations import get_location_by_slug_for_request, levels_by_short_label_for_request
|
||||||
from c3nav.mapdata.utils.user import get_user_data
|
from c3nav.mapdata.utils.user import get_user_data
|
||||||
|
@ -206,3 +211,41 @@ def change_password_view(request):
|
||||||
@login_required(login_url='site.login')
|
@login_required(login_url='site.login')
|
||||||
def account_view(request):
|
def account_view(request):
|
||||||
return render(request, 'site/account.html', {})
|
return render(request, 'site/account.html', {})
|
||||||
|
|
||||||
|
|
||||||
|
@never_cache
|
||||||
|
@login_required(login_url='site.login')
|
||||||
|
def access_redeem_view(request, token):
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
token = AccessPermissionToken.objects.select_for_update().get(id=token, redeemed=False,
|
||||||
|
valid_until__gte=timezone.now())
|
||||||
|
except AccessPermissionToken.DoesNotExist:
|
||||||
|
messages.error(request, _('This token does not exist or was already redeemed.'))
|
||||||
|
return redirect('site.index')
|
||||||
|
|
||||||
|
num_restrictions = len(token.restrictions)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
token.redeemed = True
|
||||||
|
token.save()
|
||||||
|
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
messages.info(request, _('You need to log in to unlock areas.'))
|
||||||
|
request.session['redeem_token_on_login'] = token.id
|
||||||
|
return redirect_to_login(request.get_full_path(), 'site.login')
|
||||||
|
|
||||||
|
token.redeemed_by = request.user
|
||||||
|
token.save()
|
||||||
|
|
||||||
|
messages.success(request, ungettext_lazy('Area successfully unlocked.',
|
||||||
|
'Areas successfully unlocked.', num_restrictions))
|
||||||
|
return redirect('site.index')
|
||||||
|
|
||||||
|
return render(request, 'site/confirm.html', {
|
||||||
|
'title': ungettext_lazy('Unlock area', 'Unlock areas', num_restrictions),
|
||||||
|
'texts': (ungettext_lazy('You have been invited to unlock the following area:',
|
||||||
|
'You have been invited to unlock the following areas:',
|
||||||
|
num_restrictions),
|
||||||
|
', '.join(str(restriction.title) for restriction in token.restrictions)),
|
||||||
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue