diff --git a/src/c3nav/control/__init__.py b/src/c3nav/access/__init__.py
similarity index 100%
rename from src/c3nav/control/__init__.py
rename to src/c3nav/access/__init__.py
diff --git a/src/c3nav/control/admin.py b/src/c3nav/access/admin.py
similarity index 88%
rename from src/c3nav/control/admin.py
rename to src/c3nav/access/admin.py
index b339dbba..0fed57aa 100644
--- a/src/c3nav/control/admin.py
+++ b/src/c3nav/access/admin.py
@@ -3,7 +3,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
-from c3nav.control.models import AccessOperator, AccessToken, AccessTokenInstance, AccessUser
+from c3nav.access.models import AccessOperator, AccessToken, AccessTokenInstance, AccessUser
class AccessOperatorInline(admin.StackedInline):
@@ -29,7 +29,7 @@ admin.site.register(User, UserAdmin)
class AccessTokenInline(admin.TabularInline):
model = AccessToken
show_change_link = True
- readonly_fields = ('author', 'permissions', 'description', 'creation_date', 'expires')
+ readonly_fields = ('author', 'permissions', 'description', 'creation_date', 'expires', 'expired')
def has_add_permission(self, request):
return False
@@ -55,9 +55,9 @@ class AccessTokenInstanceInline(admin.TabularInline):
@admin.register(AccessToken)
class AccessTokenAdmin(admin.ModelAdmin):
inlines = (AccessTokenInstanceInline,)
- list_display = ('__str__', 'user', 'permissions', 'author', 'creation_date', 'expires')
- fields = ('user', 'permissions', 'author', 'creation_date', 'expires')
- readonly_fields = ('user', 'creation_date')
+ list_display = ('__str__', 'user', 'permissions', 'author', 'creation_date', 'expires', 'expired')
+ fields = ('user', 'permissions', 'author', 'creation_date', 'expires', 'expired')
+ readonly_fields = ('user', 'creation_date', 'expired')
def has_add_permission(self, request):
return False
diff --git a/src/c3nav/control/migrations/0001_initial.py b/src/c3nav/access/migrations/0001_initial.py
similarity index 77%
rename from src/c3nav/control/migrations/0001_initial.py
rename to src/c3nav/access/migrations/0001_initial.py
index 4d97abf9..13d446ea 100644
--- a/src/c3nav/control/migrations/0001_initial.py
+++ b/src/c3nav/access/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10.4 on 2016-12-20 01:43
+# Generated by Django 1.10.4 on 2016-12-21 17:21
from __future__ import unicode_literals
from django.conf import settings
@@ -20,8 +20,8 @@ class Migration(migrations.Migration):
name='AccessOperator',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('description', models.TextField(verbose_name='description')),
- ('can_award_permissions', models.TextField(verbose_name='can award permissions')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='description')),
+ ('can_award_permissions', models.CharField(max_length=2048, verbose_name='can award permissions')),
('access_from', models.DateTimeField(blank=True, null=True, verbose_name='has access from')),
('access_until', models.DateTimeField(blank=True, null=True, verbose_name='has access until')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='operator', to=settings.AUTH_USER_MODEL)),
@@ -35,10 +35,11 @@ class Migration(migrations.Migration):
name='AccessToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('permissions', models.TextField(verbose_name='permissions')),
+ ('permissions', models.CharField(max_length=2048, verbose_name='permissions')),
('description', models.CharField(max_length=200, verbose_name='description')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
- ('expipres', models.DateTimeField(blank=True, null=True)),
+ ('expires', models.DateTimeField(blank=True, null=True)),
+ ('expired', models.BooleanField(default=False, verbose_name='is expired')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='creator')),
],
options={
@@ -52,8 +53,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret', models.CharField(max_length=42, verbose_name='access secret')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
- ('expipres', models.DateTimeField(null=True)),
- ('access_token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='control.AccessToken', verbose_name='Access Token')),
+ ('expires', models.DateTimeField(null=True)),
+ ('access_token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='access.AccessToken', verbose_name='Access Token')),
],
options={
'verbose_name_plural': 'Access Tokens Instance',
@@ -64,10 +65,10 @@ class Migration(migrations.Migration):
name='AccessUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('user_url', models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, verbose_name='access name')),
+ ('user_url', models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, unique=True, verbose_name='access name')),
('description', models.TextField(blank=True, max_length=200, null=True, verbose_name='description')),
('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='creation date')),
- ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='control.AccessOperator', verbose_name='creator')),
+ ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='access.AccessOperator', verbose_name='creator')),
],
options={
'verbose_name_plural': 'Access Users',
@@ -77,6 +78,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='accesstoken',
name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='control.AccessUser', verbose_name='Access User'),
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='access.AccessUser', verbose_name='Access User'),
),
]
diff --git a/src/c3nav/access/migrations/0002_auto_20161221_1739.py b/src/c3nav/access/migrations/0002_auto_20161221_1739.py
new file mode 100644
index 00000000..49f42e84
--- /dev/null
+++ b/src/c3nav/access/migrations/0002_auto_20161221_1739.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.4 on 2016-12-21 17:39
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('access', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='accesstoken',
+ name='activated',
+ field=models.BooleanField(default=False, verbose_name='activated'),
+ ),
+ migrations.AddField(
+ model_name='accesstoken',
+ name='secret',
+ field=models.CharField(default='', max_length=42, verbose_name='activation secret'),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/c3nav/control/migrations/__init__.py b/src/c3nav/access/migrations/__init__.py
similarity index 100%
rename from src/c3nav/control/migrations/__init__.py
rename to src/c3nav/access/migrations/__init__.py
diff --git a/src/c3nav/control/models.py b/src/c3nav/access/models.py
similarity index 81%
rename from src/c3nav/control/models.py
rename to src/c3nav/access/models.py
index 859a4677..e47fe80f 100644
--- a/src/c3nav/control/models.py
+++ b/src/c3nav/access/models.py
@@ -3,6 +3,8 @@ from datetime import timedelta
from django.contrib.auth.models import User
from django.db import models, transaction
+from django.db.models import Q
+from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
@@ -35,6 +37,14 @@ class AccessUser(models.Model):
verbose_name = _('Access User')
verbose_name_plural = _('Access Users')
+ @property
+ def valid_tokens(self):
+ return self.tokens.filter(Q(expired=False) | Q(expires__isnull=False, expires__lt=timezone.now()))
+
+ def new_token(self, **kwargs):
+ kwargs['secret'] = get_random_string(42, string.ascii_letters + string.digits)
+ return self.tokens.create(**kwargs)
+
def __str__(self):
return self.user_url
@@ -47,11 +57,20 @@ class AccessToken(models.Model):
description = models.CharField(_('description'), max_length=200)
creation_date = models.DateTimeField(_('creation date'), auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True)
+ expired = models.BooleanField(_('is expired'), default=False)
+ activated = models.BooleanField(_('activated'), default=False)
+ secret = models.CharField(_('activation secret'), max_length=42)
class Meta:
verbose_name = _('Access Token')
verbose_name_plural = _('Access Tokens')
+ @property
+ def activation_url(self):
+ if self.activated:
+ return None
+ return reverse('access.activate', kwargs={'pk': self.pk, 'secret': self.secret})
+
def new_instance(self):
with transaction.atomic():
for instance in self.instances.filter(expires__isnull=True):
diff --git a/src/c3nav/control/static/control/css/c3nav-control.css b/src/c3nav/access/static/access/css/c3nav-access.css
similarity index 100%
rename from src/c3nav/control/static/control/css/c3nav-control.css
rename to src/c3nav/access/static/access/css/c3nav-access.css
diff --git a/src/c3nav/access/templates/access/activate.html b/src/c3nav/access/templates/access/activate.html
new file mode 100644
index 00000000..c9b797ef
--- /dev/null
+++ b/src/c3nav/access/templates/access/activate.html
@@ -0,0 +1,14 @@
+{% extends 'access/base.html' %}
+
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block bodyclass %}login{% endblock %}
+
+{% block content %}
+ {% if success %}
+
+ {% trans 'Your access token was installed on your device!' %}
+
+ {% endif %}
+{% endblock %}
diff --git a/src/c3nav/control/templates/control/base.html b/src/c3nav/access/templates/access/base.html
similarity index 87%
rename from src/c3nav/control/templates/control/base.html
rename to src/c3nav/access/templates/access/base.html
index 6bbdc38c..d8a088f5 100644
--- a/src/c3nav/control/templates/control/base.html
+++ b/src/c3nav/access/templates/access/base.html
@@ -9,14 +9,14 @@
c3nav control panel
{% compress css %}
-
+
{% endcompress %}
-
c3nav control panel
+ c3nav access control
{% block content %}
{% endblock %}
diff --git a/src/c3nav/access/templates/access/fragment_token.html b/src/c3nav/access/templates/access/fragment_token.html
new file mode 100644
index 00000000..87cece39
--- /dev/null
+++ b/src/c3nav/access/templates/access/fragment_token.html
@@ -0,0 +1,3 @@
+{% load i18n %}
+
+{% trans 'Activate token on this device' %}
diff --git a/src/c3nav/control/templates/control/login.html b/src/c3nav/access/templates/access/login.html
similarity index 92%
rename from src/c3nav/control/templates/control/login.html
rename to src/c3nav/access/templates/access/login.html
index 43ecbcd0..b848b395 100644
--- a/src/c3nav/control/templates/control/login.html
+++ b/src/c3nav/access/templates/access/login.html
@@ -1,4 +1,4 @@
-{% extends 'control/base.html' %}
+{% extends 'access/base.html' %}
{% load bootstrap3 %}
{% load i18n %}
@@ -17,7 +17,7 @@
{% blocktrans %}This is the non-public backend for creation of auth tokens.{% endblocktrans %}
-
+
{% blocktrans %}Prove that you should have access{% endblocktrans %}
diff --git a/src/c3nav/control/templates/control/prove.html b/src/c3nav/access/templates/access/prove.html
similarity index 80%
rename from src/c3nav/control/templates/control/prove.html
rename to src/c3nav/access/templates/access/prove.html
index 213541c4..bd34a6ba 100644
--- a/src/c3nav/control/templates/control/prove.html
+++ b/src/c3nav/access/templates/access/prove.html
@@ -1,4 +1,4 @@
-{% extends 'control/base.html' %}
+{% extends 'access/base.html' %}
{% load bootstrap3 %}
{% load i18n %}
@@ -14,8 +14,10 @@
{% if success %}
{% trans 'Thanks – you get full access to the map!' %}
- {{ token }}
+ {% if replaced %}{% trans 'All previous tokens have been invalidated.' %} {% endif %}
+ {% include 'access/fragment_token.html' with token=token %}
+
{% elif hosters %}
{% if error %}
@@ -23,7 +25,7 @@
{% if error == 'invalid' %}
{% trans 'Sorry.' %} {% trans 'One or more access tokens were not correct.' %}
{% elif error == 'duplicate' %}
- {% trans 'Sorry.' %} {% trans 'You already have an access token.' %}
+ {% trans 'Sorry.' %} {% trans 'You already have a valid access token.' %}
{% endif %}
{% endif %}
@@ -33,6 +35,11 @@
{% endfor %}
+
+
+ {% trans 'Invalidate previus token(s).' %}
+
+
{% trans 'Submit' %}
diff --git a/src/c3nav/access/urls.py b/src/c3nav/access/urls.py
new file mode 100644
index 00000000..3259a946
--- /dev/null
+++ b/src/c3nav/access/urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls import url
+from django.contrib.auth import views as auth_views
+
+from c3nav.access.views import activate_token, dashboard, prove
+
+urlpatterns = [
+ url(r'^$', dashboard, name='access.dashboard'),
+ url(r'^prove/$', prove, name='access.prove'),
+ url(r'^activate/(?P[0-9]+):(?P[a-zA-Z0-9]+)/$', activate_token, name='access.activate'),
+ url(r'^login/$', auth_views.login, {'template_name': 'access/login.html'}, name='access.login'),
+ url(r'^logout/$', auth_views.logout, name='access.logout'),
+]
diff --git a/src/c3nav/access/views.py b/src/c3nav/access/views.py
new file mode 100644
index 00000000..ba1673d1
--- /dev/null
+++ b/src/c3nav/access/views.py
@@ -0,0 +1,76 @@
+from collections import OrderedDict
+
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.shortcuts import get_object_or_404, render
+
+from c3nav.access.models import AccessToken, AccessUser
+from c3nav.editor.hosters import get_hoster_for_package
+from c3nav.mapdata.permissions import get_nonpublic_packages
+
+
+@login_required(login_url='/access/login/')
+def dashboard(request):
+ return render(request, 'access/dashboard.html')
+
+
+def prove(request):
+ hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages())
+
+ if not hosters or None in hosters.values():
+ return render(request, 'access/prove.html', context={'hosters': None})
+
+ error = None
+ if request.method == 'POST':
+ user_id = None
+ for package, hoster in hosters.items():
+ access_token = request.POST.get(package.name)
+ hoster_user_id = hoster.get_user_id_with_access_token(access_token)
+ if hoster_user_id is None:
+ return render(request, 'access/prove.html', context={
+ 'hosters': hosters,
+ 'error': 'invalid',
+ })
+
+ if user_id is None:
+ user_id = hoster_user_id
+
+ replaced = False
+ with transaction.atomic():
+ user = AccessUser.objects.filter(user_url=user_id).first()
+ if user is not None:
+ valid_tokens = user.valid_tokens
+ if valid_tokens.count():
+ if request.POST.get('replace') != '1':
+ return render(request, 'access/prove.html', context={
+ 'hosters': hosters,
+ 'error': 'duplicate',
+ })
+
+ for token in valid_tokens:
+ token.expired = True
+ token.save()
+ replaced = True
+ else:
+ user = AccessUser.objects.create(user_url=user_id)
+
+ token = user.new_token(permissions=':all', description='automatically created')
+ return render(request, 'access/prove.html', context={
+ 'hosters': hosters,
+ 'success': True,
+ 'replaced': replaced,
+ 'token': token,
+ })
+
+ return render(request, 'access/prove.html', context={
+ 'hosters': hosters,
+ 'error': error,
+ })
+
+
+def activate_token(request, pk, secret):
+ token = get_object_or_404(AccessToken, expired=False, id=pk, secret=secret) # noqa
+
+ return render(request, 'access/activate.html', context={
+ 'success': True,
+ })
diff --git a/src/c3nav/control/migrations/0002_auto_20161220_0158.py b/src/c3nav/control/migrations/0002_auto_20161220_0158.py
deleted file mode 100644
index 51376a6e..00000000
--- a/src/c3nav/control/migrations/0002_auto_20161220_0158.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.4 on 2016-12-20 01:58
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('control', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='accessoperator',
- name='description',
- field=models.TextField(blank=True, null=True, verbose_name='description'),
- ),
- ]
diff --git a/src/c3nav/control/migrations/0003_auto_20161220_0214.py b/src/c3nav/control/migrations/0003_auto_20161220_0214.py
deleted file mode 100644
index fc7d7773..00000000
--- a/src/c3nav/control/migrations/0003_auto_20161220_0214.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.4 on 2016-12-20 02:14
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('control', '0002_auto_20161220_0158'),
- ]
-
- operations = [
- migrations.RenameField(
- model_name='accesstoken',
- old_name='expipres',
- new_name='expires',
- ),
- migrations.RenameField(
- model_name='accesstokeninstance',
- old_name='expipres',
- new_name='expires',
- ),
- migrations.AlterField(
- model_name='accessuser',
- name='user_url',
- field=models.CharField(help_text='Usually an URL to a profile somewhere', max_length=200, unique=True, verbose_name='access name'),
- ),
- ]
diff --git a/src/c3nav/control/migrations/0004_auto_20161220_0253.py b/src/c3nav/control/migrations/0004_auto_20161220_0253.py
deleted file mode 100644
index 7be20c2d..00000000
--- a/src/c3nav/control/migrations/0004_auto_20161220_0253.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.4 on 2016-12-20 02:53
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('control', '0003_auto_20161220_0214'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='accessoperator',
- name='can_award_permissions',
- field=models.CharField(max_length=2048, verbose_name='can award permissions'),
- ),
- migrations.AlterField(
- model_name='accesstoken',
- name='permissions',
- field=models.CharField(max_length=2048, verbose_name='permissions'),
- ),
- ]
diff --git a/src/c3nav/control/urls.py b/src/c3nav/control/urls.py
deleted file mode 100644
index d18f897f..00000000
--- a/src/c3nav/control/urls.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.conf.urls import url
-from django.contrib.auth import views as auth_views
-
-from c3nav.control.views import dashboard, prove
-
-urlpatterns = [
- url(r'^$', dashboard, name='control.dashboard'),
- url(r'^prove/$', prove, name='control.prove'),
- url(r'^login/$', auth_views.login, {'template_name': 'control/login.html'}, name='site.login'),
- url(r'^logout/$', auth_views.logout, name='site.logout'),
-]
diff --git a/src/c3nav/control/views.py b/src/c3nav/control/views.py
deleted file mode 100644
index c233e468..00000000
--- a/src/c3nav/control/views.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from collections import OrderedDict
-
-from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
-
-from c3nav.control.models import AccessUser
-from c3nav.editor.hosters import get_hoster_for_package
-from c3nav.mapdata.permissions import get_nonpublic_packages
-
-
-@login_required(login_url='/control/login/')
-def dashboard(request):
- return render(request, 'control/dashboard.html')
-
-
-def prove(request):
- hosters = OrderedDict((package, get_hoster_for_package(package)) for package in get_nonpublic_packages())
-
- if not hosters or None in hosters.values():
- return render(request, 'control/prove.html', context={'hosters': None})
-
- error = None
- if request.method == 'POST':
- user_id = None
- for package, hoster in hosters.items():
- access_token = request.POST.get(package.name)
- hoster_user_id = hoster.get_user_id_with_access_token(access_token)
- if hoster_user_id is None:
- return render(request, 'control/prove.html', context={
- 'hosters': hosters,
- 'error': 'invalid',
- })
-
- if user_id is None:
- user_id = hoster_user_id
-
- user = AccessUser.objects.filter(user_url=user_id).first()
- if user is not None:
- if user.tokens.count():
- return render(request, 'control/prove.html', context={
- 'hosters': hosters,
- 'error': 'duplicate',
- })
- else:
- user = AccessUser.objects.create(user_url=user_id)
- token = user.tokens.create(permissions=':all', description='automatically created')
- token_instance = token.new_instance()
-
- return render(request, 'control/prove.html', context={
- 'hosters': hosters,
- 'success': True,
- 'token': token_instance,
- })
-
- return render(request, 'control/prove.html', context={
- 'hosters': hosters,
- 'error': error,
- })
diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py
index 7f8d8689..d77432f0 100644
--- a/src/c3nav/settings.py
+++ b/src/c3nav/settings.py
@@ -145,10 +145,10 @@ INSTALLED_APPS = [
'c3nav.api',
'rest_framework',
'c3nav.mapdata',
+ 'c3nav.access',
'c3nav.routing',
'c3nav.site',
'c3nav.editor',
- 'c3nav.control',
]
MIDDLEWARE_CLASSES = [
diff --git a/src/c3nav/urls.py b/src/c3nav/urls.py
index ae06fd3a..e9d7f1c5 100644
--- a/src/c3nav/urls.py
+++ b/src/c3nav/urls.py
@@ -1,13 +1,13 @@
from django.conf.urls import include, url
from django.contrib import admin
+import c3nav.access.urls
import c3nav.api.urls
-import c3nav.control.urls
import c3nav.editor.urls
import c3nav.site.urls
urlpatterns = [
- url(r'^control/', include(c3nav.control.urls)),
+ url(r'^access/', include(c3nav.access.urls)),
url(r'^editor/', include(c3nav.editor.urls)),
url(r'^api/', include(c3nav.api.urls, namespace='api')),
url(r'^admin/', admin.site.urls),